4、C11标准(上):多线程支持(<threads.h>)、原子操作(<stdatomic.h>)、_Generic泛型选择

C11 标准,说实话,是 C 语言历史上一个非常重要的转折点。为什么这么说?因为从 C11 开始,C 语言终于有了官方标准的多线程和原子操作支持。在此之前,我们写并发程序全靠 POSIX 线程(pthreads)或者 Windows API,移植性差得让人头疼。我记得 2010 年左右做一个嵌入式项目,需要在 Linux 和 VxWorks 之间移植代码,光是线程相关的部分就重写了三遍……嗯,C11 的出现,很大程度上解决了这个问题。

这一章,我会带你重点看三个核心特性:多线程支持原子操作,以及一个很有意思的语法糖——_Generic 泛型选择。这三个东西,说白了就是 C11 给 C 语言开发者准备的「并发三件套」加「类型体操工具」。

C11 标准核心特性(上) 多线程支持 <threads.h> thrd_create / thrd_join mtx_init / mtx_lock cnd_wait / cnd_signal thrd_sleep / thrd_yield (类似 pthreads 但更简洁) 原子操作 <stdatomic.h> atomic_int / atomic_flag atomic_store / atomic_load atomic_fetch_add / CAS memory_order 语义 (无锁编程的基础) 泛型选择 _Generic 关键字 编译期类型分发 类似 C++ 重载的替代 类型安全宏 (C 语言自己的「重载」) 三者共同构成 C11 并发与类型安全的基础设施

4.1 多线程支持:<threads.h>

C11 的 <threads.h> 头文件,提供了创建和管理线程的标准接口。我个人觉得,它比 POSIX 线程更简洁,但功能上稍微弱一些。不过对于大多数场景,完全够用了。

4.1.1 线程的创建与等待

核心函数就两个:thrd_createthrd_join。你想想看,创建一个线程,然后等它结束,是不是很直观?

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

int worker(void *arg) {
    int id = *(int *)arg;
    printf("线程 %d 开始工作\n", id);
    // 模拟一些计算
    for (volatile int i = 0; i < 1000000; i++);
    printf("线程 %d 结束\n", id);
    return id * 2;  // 返回值可以通过 thrd_join 获取
}

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;
}
个人经验:我在项目中遇到过一个问题——忘记检查 thrd_create 的返回值。结果在资源紧张的系统上,线程创建失败,程序却继续往下跑,导致后续逻辑全乱套。所以,一定要检查返回值,这是血的教训。

4.1.2 互斥锁与条件变量

多线程编程,最头疼的就是数据竞争。C11 提供了 mtx_t 互斥锁和 cnd_t 条件变量来解决这个问题。

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

mtx_t mutex;
cnd_t cond;
int shared_data = 0;
int ready = 0;

int producer(void *arg) {
    mtx_lock(&mutex);
    shared_data = 42;
    ready = 1;
    printf("生产者: 数据已准备好\n");
    cnd_signal(&cond);  // 通知消费者
    mtx_unlock(&mutex);
    return 0;
}

int consumer(void *arg) {
    mtx_lock(&mutex);
    while (!ready) {
        cnd_wait(&cond, &mutex);  // 等待条件满足
    }
    printf("消费者: 收到数据 %d\n", shared_data);
    mtx_unlock(&mutex);
    return 0;
}

int main() {
    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);

    mtx_destroy(&mutex);
    cnd_destroy(&cond);
    return 0;
}
注意:我曾经在条件变量的使用上犯过一个经典错误——在 cnd_wait 之前没有用 while 循环检查条件,而是用了 if。结果发生了「虚假唤醒」(spurious wakeup),程序在条件不满足的情况下继续执行,导致数据不一致。记住:永远用 while 循环检查条件,这是标准做法。

4.1.3 线程本地存储

C11 还引入了 _Thread_local 关键字,用于声明线程局部变量。每个线程都有自己的副本,互不干扰。

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

_Thread_local int tls_counter = 0;

int worker(void *arg) {
    tls_counter = *(int *)arg;
    printf("线程初始值: %d\n", tls_counter);
    tls_counter++;
    printf("线程自增后: %d\n", tls_counter);
    return 0;
}

int main() {
    thrd_t t1, t2;
    int v1 = 10, v2 = 20;

    thrd_create(&t1, worker, &v1);
    thrd_create(&t2, worker, &v2);

    thrd_join(t1, NULL);
    thrd_join(t2, NULL);
    return 0;
}

你看,两个线程各自操作自己的 tls_counter,完全不会冲突。这在写日志、错误码等场景下特别有用。

4.2 原子操作:<stdatomic.h>

原子操作,说白了就是「不可分割的操作」。当一个线程在读取一个变量时,另一个线程不能同时写入它。C11 的 <stdatomic.h> 提供了原子类型和操作,让我们可以写出无锁的并发代码。

4.2.1 原子类型与基本操作

标准库定义了 atomic_intatomic_flag 等原子类型。其中 atomic_flag 是最简单的,保证是无锁的。

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

atomic_int counter = ATOMIC_VAR_INIT(0);

int increment(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, increment, NULL);
    thrd_create(&t2, increment, NULL);

    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    printf("最终计数: %d (期望: 200000)\n", atomic_load(&counter));
    return 0;
}
关键点:如果没有原子操作,两个线程同时执行 counter++ 可能会导致结果小于 200000。原子操作保证了每次增减都是完整的,不会被打断。

4.2.2 内存顺序(Memory Order)

原子操作还有一个重要的概念——内存顺序。它决定了原子操作对其他线程的可见性。C11 定义了六种内存顺序,从松散到严格:

内存顺序 含义 使用场景
memory_order_relaxed 最松散,只保证原子性 计数器,不依赖顺序
memory_order_consume 数据依赖顺序 较少使用,有争议
memory_order_acquire 防止后面的读写重排到前面 读取锁标志
memory_order_release 防止前面的读写重排到后面 写入锁标志
memory_order_acq_rel acquire + release 读-改-写操作
memory_order_seq_cst 最严格,全局顺序一致 默认值,简单但慢
#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>

atomic_int flag = 0;
int data = 0;

int writer(void *arg) {
    data = 42;                              // 普通写
    atomic_store_explicit(&flag, 1, memory_order_release);  // 释放语义
    return 0;
}

int reader(void *arg) {
    while (atomic_load_explicit(&flag, memory_order_acquire) == 0);  // 获取语义
    printf("data = %d\n", data);  // 保证能看到 42
    return 0;
}

int main() {
    thrd_t w, r;
    thrd_create(&w, writer, NULL);
    thrd_create(&r, reader, NULL);
    thrd_join(w, NULL);
    thrd_join(r, NULL);
    return 0;
}
我的建议:如果你刚开始接触内存顺序,先用默认的 memory_order_seq_cst。虽然性能可能差一点,但能避免很多诡异的 bug。等你对内存模型有更深的理解后,再考虑优化。我曾经在项目中为了追求极致性能,用了 memory_order_relaxed,结果调试了整整两天才发现是顺序问题……嗯,从那以后我就老实了。

4.3 _Generic 泛型选择

这个特性,说实话,是 C11 里我最喜欢的一个「小工具」。它让你在编译期根据表达式的类型选择不同的代码分支。说白了,就是 C 语言自己的「重载」方案。

4.3.1 基本语法

_Generic 的语法很简单:

_Generic(控制表达式, 类型1: 结果1, 类型2: 结果2, ..., default: 默认结果)

看个例子:

#include <stdio.h>

#define type_dispatch(x) _Generic((x), \
    int: "整数", \
    double: "双精度浮点数", \
    float: "单精度浮点数", \
    char *: "字符串", \
    default: "未知类型" \
)

int main() {
    int a = 10;
    double b = 3.14;
    char *c = "hello";

    printf("a 的类型: %s\n", type_dispatch(a));
    printf("b 的类型: %s\n", type_dispatch(b));
    printf("c 的类型: %s\n", type_dispatch(c));
    printf("3.14f 的类型: %s\n", type_dispatch(3.14f));

    return 0;
}

输出:

a 的类型: 整数
b 的类型: 双精度浮点数
c 的类型: 字符串
3.14f 的类型: 单精度浮点数

4.3.2 实际应用:类型安全的数学宏

我个人觉得,_Generic 最实用的场景是写类型安全的宏。比如,你想写一个通用的绝对值宏:

#include <stdio.h>
#include <math.h>

#define ABS(x) _Generic((x), \
    int: abs, \
    long: labs, \
    long long: llabs, \
    float: fabsf, \
    double: fabs, \
    long double: fabsl \
)(x)

int main() {
    int a = -10;
    double b = -3.14;
    float c = -2.5f;

    printf("ABS(-10) = %d\n", ABS(a));
    printf("ABS(-3.14) = %f\n", ABS(b));
    printf("ABS(-2.5f) = %f\n", ABS(c));

    return 0;
}
注意:我曾经看到有人用 _Generic 做类型检查,但忘了加 default 分支。结果传入一个未列出的类型时,编译直接报错。虽然这有时候是好事(强制类型安全),但如果你想要更友好的错误提示,建议加上 default: (void)0 或者一个静态断言。

4.3.3 与宏配合实现「伪重载」

你想想看,C 语言没有函数重载,但有了 _Generic,我们可以模拟出类似的效果:

#include <stdio.h>

void print_int(int x) { printf("int: %d\n", x); }
void print_double(double x) { printf("double: %f\n", x); }
void print_str(const char *x) { printf("str: %s\n", x); }

#define print(x) _Generic((x), \
    int: print_int, \
    double: print_double, \
    char *: print_str, \
    const char *: print_str \
)(x)

int main() {
    print(42);
    print(3.14);
    print("hello");
    return 0;
}

这样,一个 print 宏就能处理多种类型,代码看起来干净多了。

4.4 本章小结

C11 的这三个特性,给我的感觉是:实用、克制、有深度

  • <threads.h> 让多线程编程有了标准接口,虽然功能不如 pthreads 丰富,但胜在简洁和可移植。
  • <stdatomic.h> 为无锁编程提供了基础,内存顺序的概念虽然有点烧脑,但掌握后能写出高性能的并发代码。
  • _Generic 是个小惊喜,它让 C 语言的宏变得更智能、更安全,是类型体操的好帮手。

说实话,这三个特性在 C11 中只是「上半场」。下一章我们会继续聊 C11 的另外几个重要特性,包括匿名结构体/联合体、静态断言、以及 noreturn 函数等。嗯,先消化好这些内容,它们是你写出健壮并发代码的基础。


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