第1章:C11线程库入门——从thrd_create说起
说实话,我第一次接触C11线程库时,心里是有点抵触的。那时候我已经用POSIX线程写了五六年代码,习惯了pthread_create那一套。但后来在一个跨平台项目里,我被坑惨了——Windows上pthread模拟层跑出来的bug,查了整整三天。从那以后,我开始认真研究C11标准线程库。
嗯,今天我们就从最基础的东西聊起。C11标准给C语言带来了原生的线程支持,说白了就是不用再依赖操作系统特定的API了。你写一份代码,在Linux、Windows、macOS上都能编译通过,这才是真正的跨平台。
1.1 thrd_create:线程的起点
创建线程,是所有多线程编程的第一步。C11里用的是thrd_create,它的签名长这样:
#include <threads.h>
int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);
参数很简单:
thr:指向线程标识符的指针,相当于POSIX里的pthread_tfunc:线程入口函数,必须是int (*)(void*)这种形式arg:传给线程函数的参数
返回值是 thrd_success 或错误码。我个人习惯是创建完立刻检查返回值,别偷懒。
核心要点:线程函数返回 thrd_success(0)表示正常退出,返回其他值表示异常终止。主线程可以用 thrd_join 等待子线程结束。
来看个最简单的例子:
#include <stdio.h>
#include <threads.h>
int worker(void *arg) {
int id = *(int*)arg;
printf("线程 %d 开始干活\n", id);
return thrd_success;
}
int main(void) {
thrd_t t;
int id = 42;
if (thrd_create(&t, worker, &id) != thrd_success) {
fprintf(stderr, "创建线程失败\n");
return 1;
}
thrd_join(t, NULL);
printf("主线程结束\n");
return 0;
}
你想想看,这段代码在Linux上用gcc编译,在Windows上用MSVC编译,都能跑。这就是C11线程库的价值所在。
小技巧:我建议把线程参数用结构体封装起来。比如传多个参数时,定义一个struct,把指针传进去。这样代码清晰,也方便扩展。
1.2 mtx_t:互斥锁,保护共享数据
多线程编程里,最头疼的就是数据竞争。两个线程同时写一个变量,结果不可预测。这时候就需要互斥锁——mtx_t。
它的用法和POSIX的pthread_mutex_t几乎一样:
mtx_t mutex;
mtx_init(&mutex, mtx_plain); // 普通互斥锁
mtx_lock(&mutex);
// 临界区代码
mtx_unlock(&mutex);
mtx_destroy(&mutex);
第二个参数可以指定锁的类型:
mtx_plain:普通锁,不支持递归mtx_recursive:递归锁,同一个线程可以多次加锁mtx_timed:支持超时的锁
我曾经在一个项目中,因为用了递归锁没注意,导致死锁。排查了半天才发现是锁的嵌套层级太深。嗯,这里要注意:能用普通锁就别用递归锁,它掩盖了设计问题。
避坑指南:我曾经在嵌入式项目里,忘记调用 mtx_destroy,结果内存泄漏。虽然线程结束后锁会自动释放,但资源回收一定要养成习惯。
1.3 cnd_t:条件变量,线程间的信号灯
条件变量用来让线程等待某个条件成立。比如生产者-消费者模型里,消费者等数据,生产者通知数据来了。
C11的条件变量是 cnd_t,用法也很直观:
cnd_t cond;
cnd_init(&cond);
// 等待条件
cnd_wait(&cond, &mutex);
// 唤醒一个等待线程
cnd_signal(&cond);
// 唤醒所有等待线程
cnd_broadcast(&cond);
cnd_destroy(&cond);
注意:cnd_wait 必须在持有互斥锁的情况下调用。它会原子性地释放锁并进入等待,被唤醒后重新获取锁。
来看一个完整的例子:
#include <stdio.h>
#include <threads.h>
mtx_t mutex;
cnd_t cond;
int data_ready = 0;
int producer(void *arg) {
mtx_lock(&mutex);
data_ready = 1;
printf("生产者:数据准备好了\n");
cnd_signal(&cond);
mtx_unlock(&mutex);
return thrd_success;
}
int consumer(void *arg) {
mtx_lock(&mutex);
while (!data_ready) {
cnd_wait(&cond, &mutex);
}
printf("消费者:收到数据\n");
mtx_unlock(&mutex);
return thrd_success;
}
int main(void) {
thrd_t p, c;
mtx_init(&mutex, mtx_plain);
cnd_init(&cond);
thrd_create(&c, consumer, NULL);
thrd_create(&p, producer, NULL);
thrd_join(p, NULL);
thrd_join(c, NULL);
cnd_destroy(&cond);
mtx_destroy(&mutex);
return 0;
}
关键点:条件变量等待一定要用 while 循环检查条件,而不是 if。因为存在「虚假唤醒」——线程可能在没有收到信号的情况下被唤醒。这是POSIX和C11都有的行为,别问我怎么知道的,踩过坑。
1.4 C11线程库 vs POSIX线程:一张表说清楚
很多从POSIX转过来的朋友,会觉得C11的API名字别扭。我刚开始也这么觉得,但用顺手了发现其实更简洁。来看对比:
| 功能 | POSIX (pthread) | C11 (threads.h) |
|---|---|---|
| 创建线程 | pthread_create | thrd_create |
| 等待线程 | pthread_join | thrd_join |
| 互斥锁 | pthread_mutex_t | mtx_t |
| 条件变量 | pthread_cond_t | cnd_t |
| 线程本地存储 | pthread_key_t | tss_t |
| 一次性初始化 | pthread_once | call_once |
说白了,C11就是给这些功能换了个马甲,但核心语义完全一致。如果你熟悉POSIX,学C11线程库大概只需要半小时。
1.5 跨平台线程编程的实战心得
我参与过一个需要同时部署在Linux服务器和Windows工控机的项目。一开始用的POSIX线程,Windows上靠pthreads-w32模拟。结果呢?
- Windows上的pthread模拟层性能差,锁竞争激烈时吞吐量掉了一半
- 某些pthread扩展函数(如pthread_spinlock)在模拟层里是空的
- 调试时堆栈信息对不上,排查问题像大海捞针
后来全部改成C11线程库,代码干净了,问题也少了。虽然C11线程库功能比POSIX少一些(比如没有读写锁、屏障),但应付80%的场景足够了。
我的建议:如果你写的是纯C项目,并且需要跨平台,优先用C11线程库。如果必须用高级特性(如屏障、自旋锁),再考虑封装一层POSIX。
1.6 本章知识体系
下面这张图帮你理清C11线程库的核心脉络:
这张图把C11线程库的三个核心组件串起来了。你写代码时,基本就是这三板斧:创建线程、加锁保护数据、用条件变量做同步。
总结一下:C11线程库虽然功能不如POSIX丰富,但胜在跨平台和标准化。对于大多数应用场景,thrd_create + mtx_t + cnd_t 已经足够。记住:能用标准库就别自己造轮子,这是我在项目里用血泪换来的经验。
公众号:蓝海资料掘金营,微信deep3321