距离 C99 发布 12 年之后,ISO 终于在 2011 年推出了新一代 C 标准 C11(ISO/IEC 9899:2011)。这 12 年间,计算机走进了多核时代,而 C 语言的并发编程却仍依赖各平台私有的线程库:pthread、Windows API、各种嵌入式 RTOS 接口,代码几乎无法移植。C11 的使命,就是把"多线程"和"原子操作"写进语言标准,同时还带来了 _Generic 泛型选择、_Static_assert 静态断言等实用特性,并移除了 gets() 这个臭名昭著的不安全函数。更重要的是,标准委员会为 C11 定下的原则是:不再追求特性数量,而是优先解决"可移植的并发"这一时代痛点,同时清理 C99 遗留的不安全角落。本文围绕三大核心展开。

1. _Generic:编译期类型分派

C 语言没有函数重载,以前想"一个函数处理多种类型",只能靠宏加各种技巧,还经常因为参数类型不符产生警告。C11 的 _Generic 提供了一种标准做法:根据控制表达式的类型,在编译期选中一个关联的表达式。它最常见的用途是写类型通用的宏,或给一组函数起统一的名字,比如给整数和浮点数各写一个平方函数,再让 SQUARE(x) 自动分派。_Generic 不会求值控制表达式,只做类型匹配,因此没有运行时开销;匹配失败且没有 default 分支时,编译期直接报错,把错误挡在运行之前。_Generic 的匹配基于类型兼容,const int 与 int 需要分别列出关联;default 分支虽然可选,但强烈建议保留作为兜底。

#include <stdio.h>

// 根据实参类型选择不同字符串
#define DESCRIBE(x) _Generic((x), \
    int: "整数", \
    double: "浮点数", \
    default: "其他类型")

int main(void) {
    int a = 42;
    double b = 3.14;
    char *s = "hi";
    printf("%s\n", DESCRIBE(a)); // 整数
    printf("%s\n", DESCRIBE(b)); // 浮点数
    printf("%s\n", DESCRIBE(s)); // 其他类型
    return 0;
}
// 更实用的例子:一个名字,多个实现
int sqi(int x) { return x * x; }
double sqd(double x) { return x * x; }

#define SQUARE(x) _Generic((x), int: sqi, double: sqd)(x)

int main(void) {
    printf("%d\n", SQUARE(5));     // 25
    printf("%.1f\n", SQUARE(2.5)); // 6.2
    return 0;
}

2. _Static_assert 与对齐控制

_Static_assert 是编译期断言:条件为假时编译直接失败,并输出指定消息。它特别适合检查平台相关假设,比如"这个平台上 long 必须是 8 字节",把运行时才暴露的问题提前到编译期。C11 还规范了对齐控制:_Alignof 查询类型的对齐要求,_Alignas 指定变量的对齐方式(通常配合 <stdalign.h> 里的 alignof / alignas 别名使用),这对 SIMD 编程和底层优化很重要——很多硬件指令要求数据按 16 或 32 字节对齐。

#include <stdalign.h>
#include <stddef.h>   // size_t

// 编译期断言:条件为假时编译失败
_Static_assert(sizeof(long) >= 8, "此代码需要 64 位平台");

// 指定 16 字节对齐的缓冲区
_Alignas(16) char buffer[64];

// 查询类型的对齐要求
size_t a = _Alignof(double); // 多数平台为 8

3. 原子操作:<stdatomic.h>

多线程下,多个线程同时读写同一个变量会产生数据竞争,行为未定义。原子操作保证"读-改-写"这一整步不可分割,是互斥锁之外的另一种并发原语,性能更好,也是无锁数据结构的基础。标准库提供了 atomic_int、atomic_long 等原子类型,以及 atomic_load、atomic_store、atomic_fetch_add 等操作。内存序参数(relaxed、acquire/release、seq_cst)控制可见性保证,初学者默认使用最严格的 memory_order_seq_cst 即可。内存序值得多说两句:默认的 seq_cst(顺序一致)最容易理解,所有线程看到一致的修改顺序;relaxed 只保证原子性、不保证顺序,适合计数器这类场景;acquire/release 用于成对的"发布-订阅"同步。写应用代码时直接用默认就好,别过早优化。另外,原子类型也可以直接用 =、+= 等运算符,编译器会隐式生成对应的原子操作;不过显式调用 atomic_fetch_add 这类函数更清晰,也方便指定内存序。

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

atomic_long counter = 0;

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

int main(void) {
    thrd_t t1, t2;
    thrd_create(&t1, worker, NULL);
    thrd_create(&t2, worker, NULL);
    thrd_join(t1, NULL);
    thrd_join(t2, NULL);
    // 结果一定是 2000000,不会丢更新
    printf("counter = %ld\n", atomic_load(&counter));
    return 0;
}

4. 多线程:<threads.h>

threads.h 提供了一套精简的线程 API:thrd_create 创建线程、thrd_join 等待线程结束、mtx_t 互斥锁、cnd_t 条件变量、tss_t 线程局部存储。API 数量比 pthread 少得多,但覆盖了并发编程的基本场景,而且写出的代码在 Linux、Windows、嵌入式平台上都能编译。它的底层往往是 pthread 或 Win32 线程的薄封装,性能不必担心,只是少了读写锁、屏障等进阶工具。另一个实用的点是 tss_t 线程局部存储:每个线程有自己的一份变量副本,避免共享变量互相干扰,比如线程私有的错误码或缓存。

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

mtx_t mutex;
long counter = 0;

int worker(void *arg) {
    for (int i = 0; i < 100000; i++) {
        mtx_lock(&mutex);
        counter++;
        mtx_unlock(&mutex);
    }
    return 0;
}

int main(void) {
    thrd_t t1, t2;
    mtx_init(&mutex, mtx_plain);
    thrd_create(&t1, worker, NULL);
    thrd_create(&t2, worker, NULL);
    thrd_join(t1, NULL);
    thrd_join(t2, NULL);
    mtx_destroy(&mutex);
    printf("counter = %ld\n", counter);
    return 0;
}

5. 如何在你的环境里启用

GCC 从 4.9(2014 年)起完整支持 C11,Clang 同样支持,编译时指定 -std=c11 即可。三个注意事项:一是用到线程或原子操作时,链接阶段要加 -pthread;二是 <threads.h> 是 glibc 2.28(2018 年)才提供的,老发行版或某些小型 libc 可能没有,嵌入式交叉编译器一般自带;三是部分 32 位平台上原子操作需要额外链接 -latomic。动手前先确认一下工具链版本,能省不少排查时间。想确认编译器支持程度,查看 __STDC_VERSION__ 宏是否为 201112L 即可。

/* 编译普通 C11 程序 */
/* gcc -std=c11 -o demo demo.c */

/* 用到线程 / 原子操作时 */
/* gcc -std=c11 -pthread -o demo demo.c */
C11 特性头文件说明
泛型选择 _Generic无(关键字)编译期按类型分派
静态断言 _Static_assert无(关键字)编译期检查条件
原子操作<stdatomic.h>原子类型与原子函数
多线程<threads.h>thrd_ / mtx_ / cnd_ 系列
对齐控制<stdalign.h>_Alignas / _Alignof

6. 值不值得用

分开说:threads.h 胜在"标准、可移植",适合教学和简单工具;真要写高性能并发服务,生态与资料都更丰富的 pthread(或 C++11 的 std::thread)仍是主流选择。原子操作非常值得投入:即使不写无锁数据结构,理解原子性与内存序,也能让你更清楚锁到底在保护什么。_Generic 与 _Static_assert 是提升代码质量的低成本工具,新代码建议直接用。对嵌入式开发者来说,C11 的原子操作和线程支持通常需要目标平台的 libc 配合,选型时先确认工具链文档;对库作者来说,_Generic 让"类型安全宏"有了标准写法,值得纳入自己的工具箱。最后提醒一句:标准只是最低共识,具体平台的线程模型(如 Windows 的纤程、嵌入式的中断上下文)依然千差万别,写跨平台并发代码时多查目标平台文档。

💡 学习建议:先用 -std=c11 -pthread 把原子计数器的例子跑通,再编译一个"不用原子"的版本并加上 -fsanitize=thread,亲眼看一次数据竞争报告——这比读十遍理论都管用。