threads.h里的cnd为什么必须要配合mtx一起用?比如cnd_wait(),为什么要先获取信号量,然后再用cnd_wait()释放信号量?然后再等待?
这是一个非常棒的问题,它触及了条件变量(Condition Variables)设计的核心原因。简单来说,条件变量必须与互斥锁(mutex)配合使用,是为了避免一种称为“竞态条件(Race Condition)”的致命问题,并确保对共享数据的安全访问。
下面我们分步拆解这个问题。
1. 条件变量的核心作用
首先,要明白条件变量(cnd_t)本身不存储任何状态或数据。它不像一个计数器或一个标志位。它的唯一作用就是传递信号,让一个线程可以通知其他线程:“嘿,某个条件可能已经满足了,你们可以醒来检查一下了。”
2. “条件”是什么?
这个“条件”通常是一个基于共享数据的判断。例如:
- “队列是否为空?” (
queue.empty()) - “缓冲区是否已满?” (
buffer.full()) - “某个任务是否已完成?” (
task.completed)
这些共享数据必须被保护,否则多个线程同时读写会导致数据混乱。这就是互斥锁(mtx_t)的 job。
3. 为什么 cnd_wait 要先解锁再等待?
这是整个设计中最关键、最精妙的部分。假设我们没有互斥锁,或者 cnd_wait 不释放锁,会发生什么?
灾难场景(没有互斥锁的保护):
- 线程A 检查条件,发现条件不满足(例如,队列是空的)。
- 就在线程A准备调用
cnd_wait之前,操作系统调度走了它,转而执行线程B。 - 线程B 获取了数据(例如,向队列放入一个数据),修改了条件(队列不再为空),然后调用
cnd_signal发送信号。 - 但此时线程A还没有进入等待状态!所以这个信号丢失了(就像你在别人还没进等候室之前就喊了一声“有你的快递”,但他根本没听到)。
- 线程A 恢复执行,现在它调用
cnd_wait并进入等待。因为它错过了信号,它可能会永远等下去(死锁)。
cnd_wait 的原子操作如何解决这个问题:
cnd_wait(&cond, &mtx) 的内部执行是原子性(Atomic) 的,它做了三件事:
- 解锁互斥锁 (
mtx_unlock):释放对共享数据的锁定。 - 进入等待状态:阻塞当前线程,等待信号。
- 重新加锁 (
mtx_lock):当被cnd_signal或cnd_broadcast唤醒后,在从cnd_wait返回之前,重新获取互斥锁。
“解锁”和“等待”这两个步骤是捆绑在一起、不可分割的(原子操作)。 这意味着,不可能出现“信号已经发出,但线程还没开始等待”的竞态条件。信号要么在等待之前发出(这时互斥锁还锁着,发送信号的线程会被阻塞),要么在等待之后发出(这时线程已经在等待,能收到信号)。
4. 正确的使用模式
因此,使用条件变量的标准范式(Pattern)如下:
c// 消费者线程
mtx_lock(&mtx); // 1. 先锁住互斥锁,保护共享数据
while (condition_is_not_true) { // 2. 检查条件(必须用循环,不能用if)
// 条件不满足,等待。cnd_wait会原子地:解锁 -> 等待 -> 醒来后重新加锁
cnd_wait(&cond, &mtx);
}
// 3. 条件满足,处理数据(例如,从队列取走数据)
// ... 操作共享数据 ...
mtx_unlock(&mtx); // 4. 处理完数据,释放互斥锁
c// 生产者线程
mtx_lock(&mtx); // 1. 先锁住互斥锁,保护共享数据
// 2. 修改共享数据,使条件变为真
// ... 操作共享数据 ...
// 3. 发送信号,通知可能正在等待的消费者
cnd_signal(&cond); // 或者 cnd_broadcast(&cond);
mtx_unlock(&mtx); // 4. 释放互斥锁
总结:为什么必须配合使用?
| 组件 | 职责 | 为什么需要对方 |
|---|---|---|
互斥锁 (mtx_t) | 保护共享数据。确保任何时候只有一个线程能修改“条件”所依赖的数据。 | 需要条件变量来高效地等待条件变化,而不是通过循环检查(忙等待)浪费CPU。 |
条件变量 (cnd_t) | 传递信号。让线程可以在条件不满足时休眠,并在条件可能改变时被唤醒。 | 需要互斥锁来保护检查条件的过程和修改条件的过程,避免竞态条件,并确保“检查-等待”或“修改-发信号”的原子性。 |
一个生动的比喻:
把它们想象成一个等候室和一把钥匙。
- 共享数据:等候室里的一个公告板。
- 互斥锁 (
mtx_t):等候室的唯一一把钥匙。拿到钥匙才能进去看/修改公告板。 - 条件变量 (
cnd_t):等候室里的一个铃铛。 - 线程:人们。
流程:
- 你想查看公告板(检查条件),必须先拿到钥匙(
mtx_lock)。 - 你看到公告板上写着“资源不足”(条件不满足)。你不能一直占着钥匙在那儿盯着看(忙等待),这样别人就没法改公告板了。
- 你走进等候室,把钥匙放回门口(
cnd_wait解锁),然后坐下睡觉(cnd_wait等待)。这两个动作是一气呵成的,确保了不会有人在你睡觉的时候进不来。 - 另一个人来了,他拿到钥匙(
mtx_lock),修改了公告板(“资源已就绪”),然后摇响铃铛(cnd_signal),最后放下钥匙离开(mtx_unlock)。 - 铃铛吵醒了你。你醒来后的第一件事就是去门口拿钥匙(
cnd_wait重新加锁),然后再次查看公告板(重新检查条件)并继续工作。
如果没有这个“放下钥匙再睡觉”和“醒来先拿钥匙”的机制,整个系统就会陷入混乱和死锁。这就是为什么条件变量必须和互斥锁配合使用。