第11章:C11标准线程库——从pthreads到标准化的跨越
说实话,我第一次看到C11标准加入线程库时,心里是有点不屑的。那时候我已经用pthreads写了五六年多线程代码,心想:这玩意儿能比pthreads好用?后来在某个嵌入式项目里,我被迫用C11线程库重写了一段代码——嗯,真香。今天我就带你看看这个“后来者”到底有什么本事。
11.1 C11线程标准(threads.h)介绍
C11标准在2011年正式发布,它干了一件大事:把线程支持写进了语言标准里。你想想看,在此之前,C语言的多线程全靠第三方库——Linux用pthreads,Windows用CreateThread,写跨平台代码简直是一场噩梦。
threads.h头文件提供了什么?说白了就是一套轻量级的线程API。它包含:
- thrd_t:线程标识符类型
- thrd_create:创建线程
- thrd_join:等待线程结束
- thrd_exit:终止当前线程
- thrd_sleep:线程休眠
- thrd_yield:让出CPU
- mtx_t:互斥量类型
- cnd_t:条件变量类型
核心要点:C11线程库是标准化的最小子集,它不追求功能全面,而是追求“够用且可移植”。
我在项目中遇到过一个问题:用pthreads写的代码在Linux上跑得好好的,移植到某个RTOS上就崩了。后来改用C11线程库,虽然功能少了点,但跨平台编译一次通过。这就是标准化的力量。
11.2 thrd_create和thrd_join的使用
这两个函数是线程编程的“入口”和“出口”。先看个完整的例子:
#include <stdio.h>
#include <threads.h>
int worker(void* arg) {
int id = *(int*)arg;
printf("线程 %d 开始工作\n", id);
// 模拟一些计算
for (int i = 0; i < 3; i++) {
thrd_sleep(&(struct timespec){.tv_sec = 1, .tv_nsec = 0}, NULL);
printf("线程 %d: 第 %d 步完成\n", id, i + 1);
}
printf("线程 %d 结束\n", id);
return id * 10; // 返回值可以通过thrd_join获取
}
int main() {
thrd_t t1, t2;
int id1 = 1, id2 = 2;
int result1, result2;
// 创建两个线程
if (thrd_create(&t1, worker, &id1) != thrd_success) {
fprintf(stderr, "创建线程1失败\n");
return 1;
}
if (thrd_create(&t2, worker, &id2) != thrd_success) {
fprintf(stderr, "创建线程2失败\n");
return 1;
}
printf("主线程等待子线程结束...\n");
// 等待线程结束并获取返回值
thrd_join(t1, &result1);
thrd_join(t2, &result2);
printf("线程1返回值: %d\n", result1);
printf("线程2返回值: %d\n", result2);
printf("主线程结束\n");
return 0;
}
这段代码展示了几个关键点:
- thrd_create的第一个参数是输出参数,用来接收线程句柄
- 第二个参数是线程函数,必须是
int (*)(void*)签名 - 第三个参数是传给线程函数的参数
- thrd_join的第二个参数可以接收线程返回值,不需要就传NULL
个人习惯:我每次创建线程后都会检查返回值。thrd_create返回thrd_success才算成功,其他值都是失败。别偷懒,这个检查能帮你省下大量调试时间。
11.3 与pthreads的异同
这里我画了一张对比图,帮你快速建立认知框架:
这张图一目了然。C11线程库和pthreads的核心功能是重叠的——都能创建线程、等待线程、用互斥量保护临界区、用条件变量做同步。但pthreads像瑞士军刀,功能齐全;C11线程库像一把折叠刀,够用但不多。
具体差异我列个表:
| 功能 | C11线程库 | pthreads |
|---|---|---|
| 线程创建 | thrd_create | pthread_create |
| 线程等待 | thrd_join | pthread_join |
| 互斥量 | mtx_t / mtx_lock | pthread_mutex_t / pthread_mutex_lock |
| 条件变量 | cnd_t / cnd_wait | pthread_cond_t / pthread_cond_wait |
| 线程局部存储 | tss_t / tss_set | pthread_key_t / pthread_setspecific |
| 读写锁 | ❌ 不支持 | ✅ pthread_rwlock_t |
| 屏障 | ❌ 不支持 | ✅ pthread_barrier_t |
| 自旋锁 | ❌ 不支持 | ✅ pthread_spinlock_t |
| 信号量 | ❌ 不支持 | ✅ sem_t (semaphore.h) |
| 线程取消 | ❌ 不支持 | ✅ pthread_cancel |
| 线程调度 | ❌ 不支持 | ✅ pthread_setschedparam |
注意:C11线程库在Windows上需要MSVC 2015以上版本,在Linux上需要glibc 2.28以上。我曾经在CentOS 7上编译C11线程代码,结果发现glibc版本不够——折腾了半天才搞定。所以用之前先检查环境。
11.4 实际项目中的选择建议
你可能会问:那我到底该用哪个?我的建议是这样的:
- 写跨平台库或组件:优先用C11线程库。我在一个嵌入式项目里就是这么干的,代码在Linux、Windows、FreeRTOS上都能编译通过,省心。
- 写高性能服务器:用pthreads。它的读写锁和屏障在高并发场景下太有用了。C11线程库那点功能,说实话不够看。
- 写教学代码或简单工具:C11线程库就够了。代码更简洁,读起来也舒服。
- 维护老项目:别折腾,项目原来用啥你就用啥。我曾经试图把一个pthreads项目改成C11线程库,结果发现有些pthreads特有的功能根本找不到替代方案——最后只能改回去。
一个小技巧:如果你不确定目标平台是否支持C11线程库,可以用条件编译做兼容:
#if defined(__STDC_NO_THREADS__)
// 不支持C11线程,回退到pthreads
#include <pthread.h>
typedef pthread_t thrd_t;
// ... 其他映射
#else
#include <threads.h>
#endif
这样写出来的代码,两边都能跑。
11.5 避坑指南
最后分享几个我踩过的坑:
- 线程函数签名必须严格匹配:
int (*)(void*),多一个参数都不行。我一开始想传多个参数,结果发现只能传一个指针——那就自己封装结构体吧。 - thrd_sleep的参数是struct timespec:别搞混了,不是秒数。我见过有人直接传个整数进去,编译能过但运行结果完全不对。
- 不要忘记检查thrd_create返回值:系统资源有限,创建线程可能失败。我曾经在一个循环里创建大量线程,结果第1000个线程创建失败,程序直接崩溃——加个检查就解决了。
- thrd_join只能调用一次:对一个线程调用多次thrd_join是未定义行为。嗯,这个坑我踩过,调试了半天才发现是重复join了。
好了,C11线程库的基础就讲到这里。记住一句话:工具没有好坏,只有合不合适。C11线程库和pthreads各有千秋,关键看你项目需要什么。
公众号:蓝海资料掘金营,微信deep3321