4、C11标准(上):多线程支持(<threads.h>)、原子操作(<stdatomic.h>)、_Generic泛型选择
C11 标准,说实话,是 C 语言历史上一个非常重要的转折点。为什么这么说?因为从 C11 开始,C 语言终于有了官方标准的多线程和原子操作支持。在此之前,我们写并发程序全靠 POSIX 线程(pthreads)或者 Windows API,移植性差得让人头疼。我记得 2010 年左右做一个嵌入式项目,需要在 Linux 和 VxWorks 之间移植代码,光是线程相关的部分就重写了三遍……嗯,C11 的出现,很大程度上解决了这个问题。
这一章,我会带你重点看三个核心特性:多线程支持、原子操作,以及一个很有意思的语法糖——_Generic 泛型选择。这三个东西,说白了就是 C11 给 C 语言开发者准备的「并发三件套」加「类型体操工具」。
4.1 多线程支持:<threads.h>
C11 的 <threads.h> 头文件,提供了创建和管理线程的标准接口。我个人觉得,它比 POSIX 线程更简洁,但功能上稍微弱一些。不过对于大多数场景,完全够用了。
4.1.1 线程的创建与等待
核心函数就两个:thrd_create 和 thrd_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_int、atomic_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