C 语言线程与 pthread 实践
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。多线程能榨干多核,也能制造最隐蔽的 bug——数据竞争让计数器"莫名其妙少了",死锁让程序"卡住不动"。本文用 4 核机器上的实测数据,把线程编程的坑一个个揭开。
本文要回答的问题
- 两个线程同时
counter++,为什么会丢更新?丢多少? - 锁怎么加才对?死锁是怎么发生的?
- 条件变量解决什么问题?和忙等有什么区别?
一、pthread 基础
c
#include <pthread.h>
void *worker(void *arg) {
int *n = arg;
printf("thread %d\n", *n);
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, &arg1); // 创建线程
pthread_create(&t2, NULL, worker, &arg2);
pthread_join(t1, NULL); // 等待结束
pthread_join(t2, NULL);
}编译要加 -pthread(不只是链接库,还影响编译期宏):
bash
gcc -pthread -o app app.c二、实测:数据竞争丢更新
两个线程各对 counter 自增 100 万次,期望最终 200 万:
c
int counter = 0;
void *worker(void *arg) {
for (int i = 0; i < 1000000; i++) counter++; // 数据竞争
return NULL;
}实测(4 核,-O0):
text
无锁 counter(期望 2000000): 1027395 ← 丢了约 97 万次更新!
有锁 counter(期望 2000000): 2000000 ← 正确counter++ 是"读-改-写"三步,不是原子的:两个线程同时读到同一个值、各自 +1、写回同一个值,就"丢"了一次更新。100 万次下来,丢失了近一半。
注意:数据竞争是概率性的——
-O2或单核下可能"碰巧"不出错,这也是它最阴险的地方:测试跑得好好的,上线才崩。
三、互斥锁 mutex
c
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&mtx); // 加锁
counter++;
pthread_mutex_unlock(&mtx); // 解锁
}
return NULL;
}锁让"读-改-写"变成原子操作,但代价是串行化——临界区只能一个线程进,这是锁的性能成本(详见 内存屏障与原子)。
四、死锁
c
pthread_mutex_t m1, m2;
void *f(void *a) {
pthread_mutex_lock(&m1);
pthread_mutex_lock(&m2); // 需要 m1 和 m2
/* ... */
}
void *g(void *a) {
pthread_mutex_lock(&m2);
pthread_mutex_lock(&m1); // 顺序相反!
/* ... */
}f 拿到 m1 等 m2,g 拿到 m2 等 m1 → 互相等待,死锁。
预防:
- 固定加锁顺序(所有线程按相同顺序加锁)
- 用
pthread_mutex_trylock(拿不到就放弃,不阻塞) - 减少持锁时间(缩小临界区)
五、条件变量
线程间"等待某个条件成立"用条件变量,配合互斥锁:
c
pthread_mutex_t mtx;
pthread_cond_t cond;
int ready = 0;
// 生产者
pthread_mutex_lock(&mtx);
ready = 1;
pthread_cond_signal(&cond); // 通知等待者
pthread_mutex_unlock(&mtx);
// 消费者
pthread_mutex_lock(&mtx);
while (!ready) { // 用 while 而非 if!
pthread_cond_wait(&cond, &mtx); // 等待 + 释放锁
}
pthread_mutex_unlock(&mtx);关键坑:条件判断用 while 而不是 if——因为可能被虚假唤醒(spurious wakeup),醒来后条件可能仍不满足,必须重新判断。
六、线程安全 vs 可重入
| 概念 | 含义 |
|---|---|
| 线程安全 | 多线程并发调用不会出错(可用锁保护) |
| 可重入 | 不依赖全局/静态状态,即使被中断后重入也正确 |
可重入是更强的性质。strtok(有静态状态)既非线程安全也非可重入,用 strtok_r 替代;rand 也不是线程安全的(用 rand_r)。
七、与本站主线衔接
- 原子操作:无锁替代加锁的手段,见 内存屏障与原子。
- 线程内核机制:clone、调度、上下文切换,见 线程创建。
- 缓存一致性:锁的缓存行竞争成本,见 L3 缓存一致性。
- 伪共享:多线程下缓存行的隐形杀手,见 缓存与 C 程序性能。
一句话总结
多线程的坑:counter++ 非原子会丢更新(实测丢一半)、锁要按固定顺序加否则死锁、条件判断用 while 防虚假唤醒;掌握 pthread + mutex + cond 三件套,并牢记"能用原子操作就别用锁"。