std::condition_variable 必须与 std::unique_lock 配合使用,因其 wait() 内部需自动解锁/重锁;应优先使用带谓词的 wait(cv, []{return pred;}) 避免虚假唤醒;notify_one() 通常优于 notify_all() 以避免惊群效应。

c++中如何使用condition_variable_c++条件变量同步线程【实例】  第1张

condition_variable 必须和 unique_lock 配合使用

直接用 std::mutex 加锁后调用 wait() 会编译失败,因为 std::condition_variable::wait 只接受 std::unique_lock<:mutex>。这是强制设计,不是可选项。

原因在于:等待前必须自动释放锁,唤醒后必须自动重获锁——unique_lock 支持移动、延迟锁定、手动释放(unlock())等能力,而 lock_guard 不支持。

  • wait() 内部会先调用 unlock(),再挂起线程;被唤醒时自动调用 lock()
  • 若传入 lock_guard,编译器报错:no matching function for call to 'wait'
  • 别试图用 shared_lock 或裸指针绕过——类型不匹配,无法通过编译

wait() 的谓词版本比无参版更安全

写成 cv.wait(lock, []{ return ready; }); 而不是 cv.wait(lock);,能避免虚假唤醒(spurious wakeup)导致的逻辑错误。

无参 wait() 返回后,条件未必成立;而带谓词的版本会自动循环检查,只在条件为 true 时才退出。

立即学习“C++免费学习笔记(深入)”;

  • 虚假唤醒是 POSIX 和 C++ 标准允许的行为,不依赖系统实现,必须处理
  • 手写 while 循环 + 无参 wait 也能工作,但容易漏掉 lock.unlock() 或重复判断
  • 谓词内部访问的变量(如 ready)必须在 lambda 中按引用捕获,且保证生命周期长于等待过程

notify_one() 和 notify_all() 的选择影响性能与正确性

多数场景下优先用 notify_one();只有当多个等待线程都需响应同一事件时,才用 notify_all()

误用 notify_all() 会导致“惊群效应”:所有等待线程被唤醒,但只有一个能真正推进逻辑,其余再次进入等待——浪费调度开销,还可能引发竞争。

  • notify_one() 唤醒**至少一个**等待线程(具体哪个由实现决定,不可预测)
  • notify_all() 唤醒**所有**当前在 wait() 的线程
  • 生产者-消费者模型中,单个产品入队通常只需唤醒一个消费者,用 notify_one()
  • 若用 notify_one() 却有多个消费者在等,可能造成部分消费者永久阻塞(除非配合超时或重试机制)

完整示例:生产者通知一个消费者获取数据

下面是一个最小可行实例,展示如何用 std::condition_variable 实现线程间信号传递:

#include 
#include 
#include 
#include 
#include 

std::mutex mtx;
std::condition_variable cv;
bool ready = false;
int data = 0;

void consumer() {
    std::unique_lock lock(mtx);
    cv.wait(lock, []{ return ready; }); // 谓词确保非虚假唤醒
    std::cout << "Consumed: " << data << "\n";
    ready = false;
}

void producer() {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    {
        std::lock_guard lock(mtx);
        data = 42;
        ready = true;
    }
    cv.notify_one(); // 只唤醒一个等待者
}

int main() {
    std::thread t1(consumer);
    std::thread t2(producer);
    t1.join();
    t2.join();
}

注意 notify_one() 必须在修改共享状态(ready = true)并解锁之后调用,否则消费者可能在状态更新前就被唤醒,导致读到旧值。

真正难调试的点往往不在语法,而在“谁在什么时候改了什么变量、锁是否覆盖了全部临界区、notify 是否发生在 wait 之前”——这些靠加日志不如靠严格分段加锁+谓词等待来规避。