73 / 80 · C++11 · 约 11 分钟
条件变量:谓词、丢失通知与虚假唤醒
条件变量只负责让线程等待和重新检查,不保存事件。把真实条件存进共享状态,在同一互斥锁下修改与检查,并使用带谓词的 wait;这样通知提前发生或出现虚假唤醒,都不会破坏业务逻辑。
等待的是状态,不是通知次数
condition_variable 不是计数信号量,不会把无人接收的通知排队。若只执行一次无谓词 wait,生产者可能在消费者等待前就完成通知,消费者随后睡下,再也没有人唤醒它。正确设计把“可以继续”的事实保存为队列非空、任务完成或关闭标志。
消费者先取得互斥锁并检查状态;如果条件已经成立,就不睡眠。生产者在同一把锁下更新状态,再发出通知。通知即使早到,状态仍然留下来了。示例的谓词是 closed || !queue.empty():既支持收到数据,也支持生产结束后的退出。
wait 的原子解锁与谓词重查
wait(lock, pred) 在持锁时判断谓词,不满足才等待;等待操作把释放互斥锁和进入等待作为原子步骤,醒来后重新取得锁,再次判断。这样生产者不能插入到“已检查为假,但尚未开始等待”的危险空隙里。标准条件变量使用 unique_lock<mutex>,因为等待期间必须暂时释放锁。
等待可能虚假唤醒,多个消费者也可能竞争同一项任务:被唤醒不代表轮到当前线程时还有数据。谓词重查同时处理这两种情况,不能用一次 if 检查替代循环。单纯把 ready 改成 atomic,也不会自动修复条件检查与休眠之间的通知窗口,仍需完整的等待协议。
把关闭协议写进正常控制流
示例只有一个生产者和消费者,生产者提交三个整数后,在锁内设置 closed 并通知。消费者被唤醒后优先取完队列,只有队列为空才退出;因谓词已经成立,此时空队列意味着关闭。主线程最后 join,确保条件变量和互斥锁销毁前已无人使用它们。
通常在解锁后通知,可以减少刚醒来又争锁的机会;这不是正确性的必需条件,前提是条件变量仍然存活。一个新任务可用 notify_one,关闭时要用 notify_all 让所有等待者检查退出条件。有超时需求时应明确绝对截止时间,避免循环中反复等待完整时长导致总超时不断延长;超时返回也不等于条件已经满足。
容易答错的地方
- 不要把 notify_one 当作给下一个消费者保存的一张凭证;保存工作数量的是队列或计数状态。
- 不要在持有消费者所需互斥锁时 join 生产者,也不要在等待线程仍可能访问时销毁条件变量。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
int main() {
std::mutex mutex;
std::condition_variable changed;
std::queue<int> queue;
bool closed = false;
std::thread producer([&] {
for (int value = 1; value <= 3; ++value) {
{
std::lock_guard<std::mutex> lock(mutex);
queue.push(value);
}
changed.notify_one();
}
{
std::lock_guard<std::mutex> lock(mutex);
closed = true;
}
changed.notify_all();
});
int sum = 0;
for (;;) {
std::unique_lock<std::mutex> lock(mutex);
changed.wait(lock, [&] { return closed || !queue.empty(); });
if (queue.empty()) break;
const int value = queue.front();
queue.pop();
lock.unlock();
sum += value;
}
producer.join();
assert(sum == 6);
assert(queue.empty() && closed);
std::cout << sum << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread concurrency-condition-variable.cpp -o example && ./example预期结果
6
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
生产者若在消费者首次 wait 之前就提交三个元素并关闭,消费者会不会永久阻塞?若只写 wait(lock) 又会怎样?
查看参考答案
带谓词版本不会阻塞:首次检查就看到 closed 为真,逐次取走已有元素;队列空后退出。早先的通知是否被接收无关紧要。无谓词版本没有读取已保存状态,会直接等待,所有通知又可能已经结束,因此可能永久阻塞。修复是恢复谓词,不是增加 sleep 让生产者晚一点执行。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。