11、标准库演进(下):<threads.h>的现状与局限、<stdatomic.h>的实际应用

好,咱们接着聊标准库的演进。上一章讲了动态内存和日期时间,这一章咱们聚焦两个跟并发编程密切相关的头文件:<threads.h><stdatomic.h>

说实话,这两个头文件在 C 标准库里的地位挺有意思的。一个(<threads.h>)是 C11 才引入的,但用的人不多,甚至有些编译器到现在都没完全支持。另一个(<stdatomic.h>)也是 C11 引入的,但它在嵌入式、系统编程、高性能计算领域已经成了标配。

为什么会这样?咱们一个个来看。

11.1 <threads.h>:一个“迟到”且“尴尬”的标准

C 语言直到 C11 标准才正式引入了线程支持,也就是 <threads.h>。你想想看,Java 在 1995 年就有线程了,POSIX 线程(pthreads)在 1995 年也基本成型了。C 语言作为系统编程的“老大哥”,到 2011 年才把线程纳入标准,确实有点晚。

我个人习惯用 pthreads 做多线程开发,所以第一次看到 <threads.h> 的时候,第一反应是:“嗯,这 API 怎么这么眼熟?”

说白了,<threads.h> 就是 pthreads 的一个“瘦身版”封装。它提供了:

  • thrd_t:线程标识符
  • thrd_create():创建线程
  • thrd_join():等待线程结束
  • mtx_t:互斥锁
  • cnd_t:条件变量
  • tss_t:线程本地存储

看起来挺全的,对吧?但实际用起来,问题不少。

11.1.1 现状:支持度参差不齐

我在项目中遇到过最头疼的事:代码写好了,换了个编译器,编译不过了。

<threads.h> 的支持情况,我用一个表格给你看:

编译器/平台 支持状态 备注
GCC (Linux) 部分支持 需要链接 -lpthread,且某些实现不完整
Clang (macOS) 不支持 Apple 的 Clang 默认不提供 <threads.h>
MSVC (Windows) 支持(C11 模式) 需要 /std:c11 或更高
MinGW 不支持 需要第三方实现
嵌入式编译器 几乎都不支持 资源受限,很少用线程

你看,这支持度,说实话挺尴尬的。我建议如果你要写跨平台的多线程代码,还是老老实实用 pthreads 或者平台原生 API 吧。

11.1.2 局限:功能太“瘦”了

<threads.h> 的 API 设计,说白了就是“够用但不好用”。

举个例子,它没有提供:

  • 读写锁(pthreads 有 pthread_rwlock_t
  • 屏障(pthreads 有 pthread_barrier_t
  • 自旋锁(虽然可以用原子操作模拟)
  • 线程池(这个得自己实现)
  • 超时机制mtx_timedlock() 有,但 cnd_timedwait() 的实现不一致)

我曾经在一个项目中试图用 <threads.h> 实现一个简单的生产者-消费者模型。嗯,写到条件变量那一步就卡住了——cnd_t 的 API 太简陋,连个 cnd_broadcast() 的语义都跟 pthreads 不完全一样。

避坑指南: 我曾经在 macOS 上用 Clang 编译包含 <threads.h> 的代码,结果编译器直接报错说找不到这个头文件。后来我查了一下,Apple 的 Clang 默认不包含这个头文件,需要自己安装第三方实现(比如 mingw-w64 或者 pthreads-win32 的封装)。所以,如果你要写跨平台代码,<threads.h> 可能不是最好的选择。

11.1.3 一个简单的例子(看看就好)

虽然我不推荐在生产代码中用 <threads.h>,但为了让你有个直观感受,我还是写个例子:

#include <stdio.h>
#include <threads.h>

int worker(void *arg) {
    int id = *(int *)arg;
    printf("线程 %d 开始工作\n", id);
    thrd_sleep(&(struct timespec){.tv_sec = 1}, NULL);
    printf("线程 %d 工作结束\n", id);
    return id * 2;
}

int main() {
    thrd_t t1, t2;
    int id1 = 1, id2 = 2;
    int res1, res2;

    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;
    }

    thrd_join(t1, &res1);
    thrd_join(t2, &res2);

    printf("线程1返回: %d, 线程2返回: %d\n", res1, res2);
    return 0;
}

你看,API 风格跟 pthreads 很像,但函数名更短了(thrd_create 代替 pthread_create)。不过,这也就是个“看起来像”的程度,真要深入用,你会发现各种小坑。

11.2 <stdatomic.h>:并发编程的“基石”

<threads.h> 的尴尬处境不同,<stdatomic.h> 在 C 社区里接受度很高。为什么?因为它解决了一个实实在在的问题:多线程环境下的数据竞争

你想想看,如果没有原子操作,两个线程同时修改一个变量,结果是不确定的。用互斥锁当然可以,但锁的开销大,而且容易死锁。原子操作提供了一种轻量级的同步方式。

11.2.1 核心类型和操作

<stdatomic.h> 提供了:

  • 原子类型atomic_intatomic_longatomic_boolatomic_flag
  • 原子操作atomic_store()atomic_load()atomic_exchange()atomic_compare_exchange_strong()
  • 内存序memory_order_relaxedmemory_order_acquirememory_order_releasememory_order_seq_cst

我个人习惯把原子操作分为三类:

  1. 读写操作atomic_loadatomic_store——最基本的原子读写
  2. 交换操作atomic_exchangeatomic_compare_exchange——用于实现无锁数据结构
  3. 算术操作atomic_fetch_addatomic_fetch_sub——用于计数器、引用计数等

11.2.2 实际应用:无锁计数器

我在项目中用得最多的就是原子计数器。比如,统计某个事件发生的次数,多个线程同时递增:

#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>  // 这里只是为了演示,实际项目中建议用 pthreads

atomic_int counter = 0;

int worker(void *arg) {
    for (int i = 0; i < 100000; i++) {
        atomic_fetch_add(&counter, 1);
    }
    return 0;
}

int main() {
    thrd_t t1, t2;
    thrd_create(&t1, worker, NULL);
    thrd_create(&t2, worker, NULL);
    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    printf("最终计数: %d (期望: 200000)\n", atomic_load(&counter));
    return 0;
}

你看,用 atomic_fetch_add 代替了 counter++,就避免了数据竞争。而且性能比用互斥锁好得多——原子操作在硬件层面通常就是一条指令(比如 x86 的 LOCK XADD)。

小技巧: 如果你只是需要简单的递增/递减,atomic_fetch_addatomic_fetch_sub 就够用了。但如果你需要更复杂的操作(比如“读取-修改-写入”),atomic_compare_exchange_strong 是你的好朋友。我曾经用它在嵌入式设备上实现了一个无锁的环形缓冲区,性能提升很明显。

11.2.3 内存序:一个需要小心的话题

原子操作有一个“隐藏”的参数:内存序(memory order)。默认是 memory_order_seq_cst(顺序一致性),这是最严格但也最慢的模式。

如果你对性能有极致要求,可以考虑放宽内存序:

  • memory_order_relaxed:只保证原子性,不保证任何顺序。适合计数器这种不需要同步的场景。
  • memory_order_acquire / memory_order_release:用于实现“发布-订阅”模式。一个线程写入数据后 release,另一个线程 acquire 读取,保证看到完整的数据。
  • memory_order_seq_cst:最严格,所有线程看到的内存操作顺序一致。

我曾经在调试一个多线程程序时,发现用 memory_order_relaxed 会导致某些线程看到“过时”的数据。后来改成 memory_order_acquire/release 就解决了。嗯,这里要注意:不要轻易使用 memory_order_relaxed,除非你非常清楚自己在做什么

11.2.4 实际应用:无锁栈(简单版)

原子操作最经典的应用就是无锁数据结构。下面是一个简单的无锁栈(基于链表):

#include <stdatomic.h>
#include <stdlib.h>

typedef struct Node {
    int value;
    struct Node *next;
} Node;

typedef struct {
    atomic_int count;
    atomic_uintptr_t head;  // 用 uintptr_t 存储指针,方便原子操作
} LockFreeStack;

void push(LockFreeStack *stack, int value) {
    Node *new_node = malloc(sizeof(Node));
    new_node->value = value;

    Node *old_head;
    do {
        old_head = (Node *)atomic_load(&stack->head);
        new_node->next = old_head;
    } while (!atomic_compare_exchange_strong(
        &stack->head,
        (uintptr_t *)&old_head,
        (uintptr_t)new_node
    ));

    atomic_fetch_add(&stack->count, 1);
}

int pop(LockFreeStack *stack, int *value) {
    Node *old_head;
    Node *new_head;

    do {
        old_head = (Node *)atomic_load(&stack->head);
        if (old_head == NULL) {
            return 0;  // 栈为空
        }
        new_head = old_head->next;
    } while (!atomic_compare_exchange_strong(
        &stack->head,
        (uintptr_t *)&old_head,
        (uintptr_t)new_head
    ));

    *value = old_head->value;
    free(old_head);
    atomic_fetch_sub(&stack->count, 1);
    return 1;
}

你看,这里用 atomic_compare_exchange_strong 实现了“比较-交换”循环(CAS loop)。这是无锁编程的核心模式。

注意: 上面的代码为了简洁,省略了 ABA 问题的处理。在实际项目中,你可能需要引入“标记指针”(tagged pointer)或者使用 memory_order 来避免 ABA 问题。我曾经在一个高并发系统中因为 ABA 问题导致数据损坏,排查了整整两天才找到原因。所以,无锁编程虽然性能好,但调试起来真的很痛苦。

11.3 知识体系图

为了让你更直观地理解这两个头文件的关系和定位,我画了一张图:

C语言并发编程标准库演进 <threads.h> C11引入,基于pthreads的“瘦身版” thrd_t / thrd_create / thrd_join mtx_t / mtx_lock / mtx_unlock cnd_t / cnd_wait / cnd_signal 局限:支持度差、功能少 建议:跨平台项目慎用 <stdatomic.h> C11引入,并发编程的“基石” atomic_int / atomic_bool / atomic_flag atomic_load / atomic_store / atomic_exchange atomic_fetch_add / atomic_compare_exchange 内存序:relaxed / acquire / release / seq_cst 应用:无锁数据结构、计数器、标志位 注:箭头表示 <stdatomic.h> 是 <threads.h> 的底层基础,但两者定位不同

11.4 总结与建议

好了,咱们来收个尾。

<threads.h><stdatomic.h> 虽然都是 C11 引入的,但命运截然不同:

  • <threads.h>:定位是提供一个“标准化的线程 API”,但受限于支持度和功能缺失,实际使用价值有限。我个人建议,除非你确定目标平台完全支持,否则还是用 pthreads 或者平台原生 API 吧。
  • <stdatomic.h>:定位是提供“轻量级的同步原语”,它确实做到了。在嵌入式、高性能计算、系统编程中,原子操作几乎是标配。我建议每个 C 程序员都掌握它,至少要知道怎么用 atomic_fetch_addatomic_compare_exchange

最后,给你一个实用建议:如果你在写多线程代码,优先考虑 <stdatomic.h> 做简单的同步(计数器、标志位),用 pthreads 做复杂的同步(条件变量、读写锁)。至于 <threads.h>……嗯,先放一放吧,等它再成熟一些再说。


公众号:蓝海资料掘金营,微信deep3321