第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线程库 vs pthreads 对比 C11线程库 (threads.h) pthreads 共同点:创建/等待/互斥量/条件变量 ✅ 标准化,跨平台 ✅ 函数名简洁 ✅ 支持C11原子操作 ✅ 轻量级,适合嵌入式 ❌ 功能有限 ❌ 无读写锁 ❌ 无屏障(barrier) ❌ 无线程池支持 ❌ 部分编译器不支持 ✅ 功能丰富 ✅ 有读写锁(rwlock) ✅ 有屏障(barrier) ✅ 有自旋锁(spinlock) ✅ 线程属性可配置 ✅ 信号量支持 ❌ 非标准,平台相关 ❌ API较复杂 ❌ 命名空间污染

这张图一目了然。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 实际项目中的选择建议

你可能会问:那我到底该用哪个?我的建议是这样的:

  1. 写跨平台库或组件:优先用C11线程库。我在一个嵌入式项目里就是这么干的,代码在Linux、Windows、FreeRTOS上都能编译通过,省心。
  2. 写高性能服务器:用pthreads。它的读写锁和屏障在高并发场景下太有用了。C11线程库那点功能,说实话不够看。
  3. 写教学代码或简单工具:C11线程库就够了。代码更简洁,读起来也舒服。
  4. 维护老项目:别折腾,项目原来用啥你就用啥。我曾经试图把一个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