Item39:对于一次性事件通信考虑使用void的futures
一次性事件通信
- 方向:检测任务 → 反应任务(单方向);
- 内容:仅传递 “事件已发生” 的信号,无需传递具体数据;
- 次数:仅一次(事件触发后无需重复通知);
- 要求:反应任务需真正阻塞(不占用 CPU)、无通知丢失、无虚假唤醒。
方案 1:条件变量(condvar)—— 设计别扭的 “标配”
1. 多余的互斥锁(代码异味)
条件变量必须配合互斥锁使用(C++11 API 强制要求),但一次性事件通信中,检测 / 反应任务往往无共享数据需要保护(比如检测任务初始化完数据结构后不再访问,反应任务仅在事件后访问)—— 互斥锁成了 “无意义的强制依赖”。
2. 通知丢失风险
若检测任务在反应任务调用 cv.wait() 前执行 cv.notify_one(),反应任务会永久挂起(因为 wait 时通知已丢失)。
3. 虚假唤醒问题
条件变量存在 “虚假唤醒”(即使未被通知,wait 也可能返回),因此必须配合 “条件判断” 使用:
1 | std::condition_variable cv; |
但问题在于:反应任务本就是 “等待事件发生”,若它能自行判断 flag,就无需依赖条件变量 —— 逻辑上形成矛盾。
结论:条件变量适合 “重复事件 + 共享数据保护” 场景,不适合一次性无数据的事件通信。
方案 2:共享布尔 flag —— 无锁但耗资源的轮询
为规避条件变量的问题,很多开发者会用 std::atomic<bool> 实现 “flag 轮询”:
1 | std::atomic<bool> eventOccurred(false); |
优点
- 无需互斥锁,设计简单;
- 无通知丢失(无论检测任务何时置位 flag,反应任务轮询总能读到);
- 无虚假唤醒(仅依赖 flag 值判断)。
缺点
反应任务的 while (!eventOccurred) 是忙等(busy waiting) —— 线程看似 “阻塞”,实则一直在占用 CPU 核心运行,会导致:
- 浪费硬件线程资源(本可给其他任务使用);
- 增加上下文切换开销;
- 持续占用核心,无法进入低功耗状态(嵌入式 / 移动端尤其明显)。
结论:flag 轮询仅适合 “事件极短时间内触发” 的场景,不适合长时间等待。
方案 3:条件变量 + flag —— 能工作但不优雅的组合
将前两种方案结合,可解决各自的问题:
1 | std::condition_variable cv; |
优点
- 解决了条件变量的通知丢失、虚假唤醒问题;
- 解决了 flag 轮询的 CPU 占用问题(反应任务真正阻塞)。
问题
设计逻辑割裂:检测任务需 “先置位 flag + 再通知 cv”,反应任务需 “等 cv 唤醒 + 再查 flag”—— 本质是 “用两个操作传递一个事件信号”,代码不自然、可读性差,且仍需维护多余的互斥锁。
结论:能工作,但不是最优解。
方案 4:void promise/future —— 一次性通信的最优解
std::promise 和 std::future 原本用于 “异步任务返回结果”,但可无缝适配 “仅传递事件信号” 的场景 —— 思路是:用 void 类型表示 “仅传递事件发生的信号,无数据”。
实现(单反应任务)
1 | std::promise<void> eventPromise; // 检测任务持有:通信信道的写入端 |
优势
- 无多余依赖:无需互斥锁、无需 flag,代码极简;
- 无通知丢失:无论检测任务何时调用
set_value(),反应任务后续调用wait()都会立即返回; - 无虚假唤醒:future 的 wait 仅在
set_value()触发后返回,无任何无意义唤醒; - 真正阻塞:反应任务 wait 时完全释放 CPU,无轮询开销,低功耗、低上下文切换;
缺点
- 堆内存开销:promise/future 的 “共享状态”是堆上动态分配的,存在少量内存分配 / 释放开销;
- 一次性限制:一个 promise 只能调用一次
set_value(),无法重复用于多次事件通信(这也是 “一次性事件通信” 的场景匹配项)。
扩展:多反应任务的一次性通知
若需一个检测任务通知多个反应任务,只需将 std::future<void> 转为 std::shared_future<void>(共享状态可被多个线程引用):
1 | std::promise<void> eventPromise; |
潜在风险:异常导致的线程永久挂起
若用 RAII(如 ThreadRAII)管理反应线程,需注意:若检测任务在调用 set_value() 前抛出异常,promise 未被触发,反应线程的 wait() 会永久阻塞—— 最终导致 RAII 对象析构时调用 join() 也永久挂起。
解决思路(参考ThreadRAII + Thread Suspension = Trouble?):
- 给 promise 增加 “取消机制”(比如结合
std::exception_ptr传递异常); - 用超时 wait 替代无限 wait(
future.wait_for()); - 捕获检测任务的异常,主动触发 promise 的
set_exception()。
总结
- 条件变量适合 “重复事件 + 共享数据保护”,但一次性通信中存在多余互斥锁、通知丢失、虚假唤醒问题;
- 共享 flag 无锁但轮询耗资源,仅适合极短时间等待的场景;
- 条件变量 + flag 能工作,但设计不优雅、逻辑割裂;
void promise/future是一次性事件通信的最优解:无锁、无虚假唤醒、真正阻塞、代码极简,仅需接受 “堆内存开销 + 一次性通信” 的限制。
