1. 为什么有了互斥锁还不够
1.1 互斥锁管不了“等待”
上一篇文章里我们聊了互斥锁,解决了多个线程同时访问共享数据时“打架”的问题。但实际写多线程程序的时候,很快会碰到另一个场景:一个线程在等某个条件成立,而这个条件要由另一个线程去改变。比如生产者往队列里放数据,消费者必须等队列里有数据才能取;读写缓存时,读线程要等写线程把数据准备好。这类问题光靠互斥锁是搞不定的,因为互斥锁只保证“同一时刻只有一个线程访问”,它并不提供“当条件不满足时让线程停下来,等条件满足再继续”的机制。
我刚开始学多线程的时候也踩过这个坑,总觉得只要把所有共享变量都拿锁保护起来就万事大吉。结果写出来的消费者线程只能不停地轮询,CPU占用直接拉满。后来才明白,线程之间不仅需要互斥,还需要同步:同步的意思是让线程之间按照某种顺序或时机协作,条件变量就是专门干这个的。
1.2 轮询方案有多糟
很多人第一反应是:条件不满足,我就用一个 while 循环去检查嘛。代码写出来大概是这样的:
c复制pthread_mutex_lock(&lock);
while (queue_empty(&q)) {
pthread_mutex_unlock(&lock);
usleep(1000);
pthread_mutex_lock(&lock);
}
item = queue_pop(&q);
pthread_mutex_unlock(&lock);
这样确实能工作,但问题很明显:消费者在条件不满足的时候会反复尝试拿锁、查条件、放锁,再睡一会儿,再重来。哪怕队列里已经有数据了,消费者也可能还在睡觉,多等 1 毫秒甚至更长。这个例子里 1 毫秒看似还好,但如果队列没有数据的时间很长,或者消费者很多,就会白白空转,浪费 CPU。而且轮询间隔调长了,数据来了响应不及时;调短了,CPU 占用又上去了。你永远在“响应延迟”和“性能损耗”之间做痛苦的权衡。
更隐蔽的问题是,如果在两个线程之间用轮询传递状态,程序逻辑往往被拆得七零八落,时间一长自己都看不懂了。条件变量就是用来替代这种“轮询+睡眠”模式的:线程在条件不满足时真正地阻塞挂起,不占 CPU;当条件变化时,由另一个线程显式地通知它醒来。这就像你去餐厅吃饭,不用一直盯着厨师有没有做完你的菜,而是拿了个叫号器,做好了会震动提醒你,这才合理。
1.3 条件变量到底是什么
条件变量这个名字第一次听很抽象,我的理解是:它本身不保存任何状态,也没有值,它只是一个“通知队列”。当线程 A 发现某个条件不满足时,可以调用 pthread_cond_wait 把自己放到这个通知队列上,然后乖乖睡觉。线程 B 在改变了条件之后,调用 pthread_cond_signal 或 pthread_cond_broadcast,把队列上的一个或全部线程唤醒。
这里要特别强调:条件变量必须配合互斥锁一起使用,而且“检查条件”和“进入睡眠”必须是一个原子操作。为什么?因为如果线程 A 检查到条件不满足,正打算去睡觉,这时线程 B 抢先把条件改好了并发了通知,然后线程 A 才睡着,那这个通知就白白丢了,线程 A 可能永远醒不过来。把这两个操作放到同一个锁的临界区内,就是为了防止这种“检查与睡眠之间被插队”的竞态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 条件变量核心API与原理
2.1 创建和销毁
条件变量的类型是 pthread_cond_t。在 Linux 下用 pthread 系列函数操作,使用前需要初始化,使用后需要销毁。
最简单的两种初始化方式:
c复制// 方式一:静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 方式二:动态初始化
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
第二个参数是 attr,一般传 NULL 使用默认属性。动态初始化的好处是可以在运行时设置条件变量的一些属性,比如跨进程共享。跨进程使用时需要把 attr 的 process-shared 属性设置为 PTHREAD_PROCESS_SHARED,并且条件变量要放在共享内存里。这个场景比较少见,通常我们做线程同步用默认的就够了。
销毁函数:
c复制pthread_cond_destroy(&cond);
注意:如果有线程正在等待这个条件变量,销毁行为是未定义的。所以正确方式是先确保所有等待者都被唤醒并退出等待,再执行 destroy。哪怕只有一个线程还在 wait,你把它 destroy 了,程序很可能在后续运行时崩溃或卡死。
2.2 等待与唤醒:wait、signal、broadcast
核心三个函数不用记太多参数:
c复制int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_wait 的行为要拆成三步理解:
- 调用前,调用线程必须已经持有 mutex。
- 调用时,函数会原子地把 mutex 释放,然后把线程挂起到 cond 的等待队列上。
- 当线程被唤醒时,函数会重新获取 mutex,然后返回。
这三步是设计的关键。很多人一开始不理解为什么 wait 要传 mutex,其实就是因为需要“原子地释放锁+睡眠”,避免上述信号丢失问题。
pthread_cond_signal 一次唤醒一个等待线程;pthread_cond_broadcast 唤醒所有等待线程。这里“唤醒”只是把线程从等待队列移到就绪队列,并不代表线程马上能继续执行,它们仍然要竞争 mutex。每次 signal 唤醒的线程是不确定的,如果没有线程在等待,signal 就没有效果,这个被称为“信号丢失问题”,后
