c++怎么使用std::condition_variable实现任务触发_c++ 生产者唤醒逻辑【详解】

std::condition_variable必须与std::mutex配合使用,所有共享状态访问和wait/notify操作均需在同锁保护下进行;wait必须用while循环防止虚假唤醒;notify_one适用于单任务唤醒,notify_all仅用于广播场景;需妥善管理生命周期避免死锁。

std::condition_variable 必须和 std::mutex 一起用

单独声明 std::condition_variable 没有意义,它不能自己保护共享状态。所有对共享变量的读写、以及 wait() / notify_one() 调用,都必须在同一个 std::mutex 的保护下进行,否则会触发未定义行为(比如程序崩溃或唤醒丢失)。

常见错误是:生产者改完任务队列后只加锁解锁,却在锁外调用 cv.notify_one() —— 这会导致唤醒时机错位,消费者可能永远阻塞。

  • 正确做法:在持有 std::unique_lock<:mutex> 的前提下调用 cv.notify_one()cv.notify_all()
  • 不需要等锁释放后再唤醒;notify_* 是线程安全的,且不依赖锁是否仍持有
  • 但必须确保:通知前,共享状态(如队列非空)已更新完毕,并仍在锁保护中

wait() 必须用 while 循环包裹,不能用 if

std::condition_variable::wait() 可能被虚假唤醒(spurious wakeup),也可能在唤醒后发现条件又变了(比如另一个线程抢走了任务)。所以永远不要写 if (queue.empty()) cv.wait(...)

std::unique_lock lk(mtx);
while (tasks.empty()) {
    cv.wait(lk); // 自动释放锁,唤醒后重新获取
}
auto task = std::move(tasks.front());
tasks.pop();
// 此时可确信 tasks 非空,且已从队列取出一个任务
  • 使用 while 是强制约定,不是优化建议
  • lambda 形式的 wait(lk, []{ return !tasks.empty(); }) 本质也是 while 循环,更简洁但逻辑相同
  • 如果用 if,一旦发生虚假唤醒,就会尝试从空队列取任务,导致 front()pop() 崩溃

生产者唤醒逻辑:notify_one() vs notify_all()

多数场景下用 cv.notify_one() 就够了——只要有一个消费者在等,就唤醒一个;多个消费者等待时,由系统调度决定唤醒谁。用 notify_all() 会唤醒全部等待线程,它们会竞争锁和任务,造成“惊群”开销,尤其在高并发下明显。

  • 仅当需要广播某种全局状态变更(比如关闭信号、重置配置)时才用 notify_all()
  • 生产者每次提交一个任务,对应唤醒一个消费者即可,匹配语义清晰
  • 注意:唤醒操作本身不保证被唤醒线程立刻执行;它只是让线程脱离 wait 阻塞,之后仍需竞争 mutex

避免死锁和资源泄漏的关键细节

最易被忽略的是:消费者线程退出前,必须确保不会遗漏未处理的通知;生产者销毁前,应停止提交新任务,并通过 notify_all() 唤醒所有等待线程,让它们检查退出条件。

  • 推荐在共享状态中加入 bool shutdown_requested = false;,消费者循环里同时检查 !tasks.empty() || shutdown_requested
  • 生产者结束前:设 shutdown_requested = true;,再调用 cv.notify_all()
  • 不要在析构函数里直接调用 notify_* —— 若此时消费者线程正在析构或已退出,wait() 可能已失效,引发未定义行为
  • std::condition_variable 本身无需手动清理,但关联的 std::mutex 和共享数据生命周期必须严格管理
实际用起来,最难的不是语法,而是把「状态变更」、「锁保护」、「通知时机」三者在时间轴上对齐。漏掉任意一环,都会出现偶发卡死或崩溃。