99 / 163 · C++20 · 约 13 分钟
条件变量等待的是状态,不是一次通知
一次性计算交接需要同时证明状态可见、等待不丢失以及工作线程不会越过对象寿命。用互斥量保护谓词,再用 C++20 jthread 管理结束,通知早到或晚到都不会改变答案。
C++ Concurrency in Action
完整阅读 2012 年第一版的第 1–10 章、附录 A–D(正文书页 1–486);另读第二版未登录预览的第 1、2 章可见部分,第二版不声称读完。第一版公开 PDF 的网页转换在书页 194 中断,改用私有本地文本继续读至附录 D 结尾;目录和索引不计正文。历史 API 与示例并非 C++20 规范,本文不照搬无锁容器实现。
查看版本、实际阅读范围与原文入口 →把完成事实留在状态里
主线程需要等工作线程算出答案。最脆弱的设计是先睡一小段时间,再假定计算已经完成:调度器没有承诺工作线程在这段时间内得到运行机会。另一个误区是把 notify_one 当成可积累的消息;没有等待者时通知不会留下一张票。
我们把答案和 ready 放在同一把互斥量的保护下。工作线程先写答案,再令 ready 为真,随后解锁并通知。主线程用带谓词的 wait 检查 ready。如果通知早已发生,谓词已经为真,根本不需要阻塞;如果尚未完成,wait 会原子地释放锁并进入等待。完成事实存在于 ready,而不是通知次数。
唤醒不等于条件成立
等待允许虚假唤醒;即使确实有人通知,也不能由此推导我们要的业务条件已经成立。因此应使用 wait(lock, predicate),它会在持锁状态下反复检查谓词,直到结果为真才返回。这里的锁必须是可暂时释放并重新取得的 unique_lock,而不是仅负责作用域释放的 lock_guard。
主线程读取 answer 时仍持有同一把锁。工作线程解锁与随后获得该锁建立同步关系,使写入对读取可见。ready 无须再改成 atomic:只要所有访问始终由同一互斥量保护,普通 bool 就足够。把谓词移到锁外或混用另一把锁,会破坏这个证明,不能靠额外 notify 修复。
等待完成与对象存活分别证明
条件变量解决状态交接,并不自动管理工作线程。示例在同步对象之后构造 jthread,所以逆序析构时先等待工作线程结束,再销毁它引用的互斥量与条件变量。使用显式内层作用域也可以让这个关系更醒目;不得把线程 detach 后任由引用指向已经离开的栈。
原书第一版以 C++11 的 thread 和自制 RAII 守卫解释生命周期,本例改用 C++20 jthread。这不是强制取消:析构提出停止请求后仍要等待线程自行返回。示例任务是必定完成的小计算,不需要关闭协议;若改成无限消费队列,必须设计关闭状态、唤醒和退出条件。反复运行输出正确只能辅助观察,正确性最终来自状态与同步关系。
常见误区
- notify_one 不是持久消息;不要用一次裸 wait 代替状态谓词。
- jthread 不会强行终止死循环;自动 join 仍可能无限等待,不应在等待者持有工作线程所需锁时析构它。
运行一个例子
最低标准 C++20 · 完整程序 · 下载 .cpp
#include <cassert>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
int main() {
std::mutex mutex;
std::condition_variable changed;
bool ready = false;
int answer = 0;
{
std::jthread worker([&] {
const int computed = 6 * 7;
{
std::lock_guard lock(mutex);
answer = computed;
ready = true;
}
changed.notify_one();
});
std::unique_lock lock(mutex);
changed.wait(lock, [&] { return ready; });
assert(answer == 42);
std::cout << answer << "\n";
}
}
在本地编译
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread books-cpp-concurrency-in-action.cpp -o example && ./example预期结果
42
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
工作线程在主线程调用 wait 前已经完成并通知,主线程为什么仍能退出?如果只保留通知而删除 ready 呢?
查看参考答案
wait 的谓词重载先在持锁状态下检查 ready。工作线程已将它置真,主线程取得锁后可见该写入,因而直接返回。若没有持久谓词,之前的通知不会缓存,后来的裸 wait 可以永久阻塞;即使它偶然虚假唤醒,也不能据此证明答案已写好。
继续查证
- 原书第一版 Chapter 4: Synchronizing concurrent operations
- std::condition_variable::wait
- std::jthread::~jthread
标准草案与官方章节会更新;版本标记只说明示例最低要求。