std::condition_variable通过“等待-通知”机制解决线程间协作问题,避免忙等。它需与std::mutex和std::unique_lock配合使用,wait()在条件不满足时释放锁并休眠,notify_one()/notify_all()唤醒等待线程,常用于生产者-消费者模型、线程池调度等场景,核心是避免虚假唤醒和丢失通知,确保高效安全的线程同步。

在C++多线程编程中,
std::condition_variable是实现线程间同步和通信的关键工具,它允许一个或多个线程等待某个特定条件成立,而不会像忙等(busy-waiting)那样持续消耗CPU资源。简单来说,它提供了一种高效的“等待-通知”机制,让线程在条件不满足时休眠,在条件满足时被唤醒。
解决方案
要使用
std::condition_variable进行线程同步,你通常需要配合
std::mutex和
std::unique_lock<std::mutex>。核心思想是:当一个线程需要等待某个条件时,它会获取一个互斥锁,然后调用条件变量的
wait()方法。
wait()方法会原子性地释放互斥锁并使线程进入休眠状态。当另一个线程改变了条件并希望唤醒等待的线程时,它也会获取互斥锁,修改条件,然后调用条件变量的
notify_one()或
notify_all()方法。被唤醒的线程会重新获取互斥锁,并检查条件是否真的满足(因为可能存在虚假唤醒),如果满足则继续执行,否则再次等待。
下面是一个经典的生产者-消费者模型示例,它清晰地展示了
std::condition_variable的使用:
#include <iostream>
#include <vector>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
// 共享资源
std::mutex mtx; // 保护共享数据
std::condition_variable cv; // 条件变量
std::queue<int> data_queue; // 共享数据队列
bool stop_producing = false; // 停止生产的标志
void producer() {
for (int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时
{
std::unique_lock<std::mutex> lock(mtx); // 获取锁
data_queue.push(i); // 生产数据
std::cout << "Producer pushed: " << i << std::endl;
cv.notify_one(); // 通知一个等待的消费者
} // 锁在这里自动释放
}
// 生产完毕,通知所有消费者可以停止等待了
{
std::unique_lock<std::mutex> lock(mtx);
stop_producing = true; // 设置停止标志
std::cout << "Producer finished production, notifying all consumers." << std::endl;
} // 锁在这里自动释放
cv.notify_all(); // 唤醒所有等待的消费者
}
void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock(mtx); // 获取锁
// 等待条件:队列不为空 或者 生产者已停止
// wait()函数会自动释放锁并休眠,被唤醒时会重新获取锁
cv.wait(lock, [&]{ return !data_queue.empty() || stop_producing; });
// 如果队列为空且生产者已停止,说明没有更多数据了,消费者可以退出了
if (data_queue.empty() && stop_producing) {
std::cout << "Consumer " << id << " finished." << std::endl;
break;
}
// 处理数据
int data = data_queue.front();
data_queue.pop();
std::cout << "Consumer " << id << " consumed: " << data << std::endl;
}
}
int main() {
std::thread prod_thread(producer);
std::thread cons_thread1(consumer, 1);
std::thread cons_thread2(consumer, 2); // 多个消费者
prod_thread.join();
cons_thread1.join();
cons_thread2.join();
std::cout << "All threads finished." << std::endl;
return 0;
}这段代码里,生产者线程在每次生产完数据后,会通过
cv.notify_one()唤醒一个消费者线程。当所有数据生产完毕,它会设置
stop_producing标志,并通过
cv.notify_all()唤醒所有消费者,告诉它们“活儿干完了,可以收工了”。消费者线程则在
cv.wait()中等待,直到队列中有数据或者生产者发出停止信号。这个
wait的第二个参数,也就是lambda表达式,是一个谓词(predicate),它会在
wait返回前被检查,这能有效避免虚假唤醒带来的问题。
立即学习“C++免费学习笔记(深入)”;
为什么我们需要std::condition_variable
?它解决了哪些并发难题?
说实话,在多线程编程里,光有互斥锁(
std::mutex)是远远不够的。互斥锁只能保证同一时间只有一个线程访问共享资源,避免数据竞争。但很多时候,线程之间不仅仅是“互斥”的关系,它们还需要“协作”。比如,一个线程需要等待另一个线程完成某个任务,或者等待某个条件满足才能继续执行。
如果没有
std::condition_variable,我们可能会怎么做?最直观的,可能就是忙等(busy-waiting)了。一个线程会不断地去检查某个共享变量,比如:
// 糟糕的忙等示例
bool data_ready = false;
void consumer_bad() {
while (!data_ready) {
// 什么也不做,或者短暂休眠
std::this_thread::sleep_for(std::chrono::microseconds(1)); // 稍微好一点,但仍然是忙等
}
// 处理数据
}这种方式的弊端非常明显:它会白白消耗大量的CPU周期,即使条件不满足,线程也一直在运行,浪费资源。在实际项目中,这简直是性能杀手。
std::condition_variable正是为了解决这种“等待某个条件”的协作问题而生的。它让等待的线程可以高效地进入休眠状态,释放CPU资源,直到被明确地通知才会被唤醒。它主要解决了以下几类并发难题:
- 生产者-消费者问题: 这是最经典的场景。生产者生产数据,消费者消费数据。如果队列空了,消费者就得等;如果队列满了(在有界队列中),生产者就得等。条件变量完美地协调了这种等待。
- 线程池任务调度: 线程池中的工作线程需要等待任务队列中有新的任务到来。如果没有任务,它们就休眠;有新任务了,就被唤醒去执行。
- 一次性事件通知: 比如一个主线程启动了多个子线程去执行任务,然后主线程需要等待所有子线程都完成初始化或者某个特定阶段后才能继续。
- 资源可用性等待: 线程需要等待某个共享资源变得可用,例如文件句柄、网络连接等。
-
优雅的线程终止: 就像上面示例中,通过一个标志位和
notify_all
,可以通知所有等待的线程安全地退出。
在我看来,
std::condition_variable是构建高效、响应式并发程序的基石之一。它把“等待”从低效的轮询检查,变成了高效率的事件驱动,这在现代多核系统中尤其重要。
std::condition_variable
的核心机制:wait()
和 notify()
的工作原理
理解
wait()和
notify()的工作原理,是正确使用
std::condition_variable的关键。它们并不是简单地让线程睡着或醒来,背后有一套精妙的原子操作。
wait()
的工作原理:
当一个线程调用
cv.wait(lock, predicate)时,它的内部流程大致是这样的:
-
检查谓词: 首先,
wait()
会检查你提供的谓词(lambda表达式)。如果谓词返回true
,说明条件已经满足,线程就不需要等待,直接返回,并保持锁的持有状态。 -
原子性释放锁并休眠: 如果谓词返回
false
(或者你没有提供谓词,直接调用cv.wait(lock)
),wait()
会原子性地执行两个操作:- 释放
lock
(std::unique_lock
对象持有的互斥锁)。 - 将当前线程放入条件变量的等待队列中,并使其进入休眠状态(阻塞)。 这个原子性非常重要,它确保了在锁被释放和线程进入休眠之间,不会有其他线程在没有获取锁的情况下修改条件变量,从而避免了丢失通知(lost wakeup)的风险。
- 释放
-
被唤醒并重新获取锁: 当其他线程调用
notify_one()
或notify_all()
时,等待队列中的线程会被唤醒。被唤醒的线程会尝试重新获取之前释放的互斥锁。 -
再次检查谓词: 成功获取锁后,
wait()
会再次检查谓词。- 如果谓词返回
true
,线程就从wait()
调用中返回,继续执行后续代码。 - 如果谓词返回
false
,线程会再次释放锁,并重新进入休眠状态。 这个重复检查谓词的机制,正是为了处理虚假唤醒(spurious wakeups)。虚假唤醒是指线程在没有收到notify
信号的情况下,或者在条件尚未满足时,被操作系统调度器错误地唤醒。虽然不常见,但标准允许这种情况发生,所以我们必须用一个while
循环(或者wait
的谓词参数)来包裹条件检查,确保只有在条件真正满足时才继续执行。
- 如果谓词返回
notify_one()
和notify_all()
的工作原理:
当一个线程调用
notify_one()或
notify_all()时,它的内部流程相对简单:
-
notify_one()
:- 从条件变量的等待队列中选择一个(通常是第一个)等待的线程。
- 唤醒这个线程,使其从休眠状态变为可运行状态。被唤醒的线程会去尝试重新获取互斥锁。
- 选择哪个线程是未定义的行为,你不能依赖特定的顺序。
-
notify_all()
:- 唤醒条件变量等待队列中的所有等待线程。
- 所有被唤醒的线程都会尝试重新获取互斥锁。
何时使用notify_one()
,何时使用notify_all()
?
-
notify_one()
: 当你确切知道只有一个线程需要处理这个条件时,或者有多个消费者,但每次只生产一个物品,只唤醒一个消费者就足够了,避免不必要的线程上下文切换。例如,上面生产者-消费者模型中,每生产一个数据,就notify_one()
。 -
notify_all()
: 当多个线程可能需要响应同一个条件时,或者你无法确定哪个线程需要被唤醒时。例如,当一个全局状态改变,所有等待这个状态的线程都需要重新评估时;或者在线程池中,当有多个任务被加入队列,但你不知道哪些工作线程空闲时;以及在上面示例中,生产者停止生产时,需要通知所有消费者检查stop_producing
标志。notify_all()
在关闭(shutdown)场景下也特别有用,可以确保所有等待的线程都能检查到退出条件并优雅退出。
选择正确的通知方式,既能保证程序的正确性,也能在一定程度上影响性能。
使用 condition_variable
时常见的陷阱与最佳实践
虽然
std::condition_variable功能强大,但它也是一个容易出错的同步原语。一些常见的陷阱如果没注意到,轻则程序行为异常,重则死锁或数据损坏。
常见的陷阱:
-
没有使用互斥锁保护共享条件: 这是最基础也是最致命的错误。条件变量本身不保护共享数据。你必须使用
std::mutex
来保护所有被条件变量依赖的共享数据(例如示例中的data_queue
和stop_producing
)。如果没有锁,多个线程同时修改条件,会导致数据竞争,程序行为不可预测。 -
忘记了
wait()
的谓词或while
循环: 就像前面提到的,wait()
可能会发生虚假唤醒。如果你只是简单地if (!condition) cv.wait(lock);
,那么在虚假唤醒后,线程会错误地认为条件已满足并继续执行,导致逻辑错误。始终使用cv.wait(lock, [&]{ return condition; });或者while (!condition) { cv.wait(lock); }。 -
在不持有锁的情况下修改条件或调用
notify()
: 修改共享条件必须在持有互斥锁的情况下进行。如果你在没有锁的情况下修改了条件,然后调用notify()
,那么一个等待的线程可能在条件被修改和notify()
之间进入等待状态,从而错过通知,导致永久休眠(lost wakeup)。notify()
本身可以在不持有锁的情况下调用(虽然通常推荐在持有锁时调用,因为这样可以确保条件在通知时是稳定的),但修改条件变量所依赖的共享状态必须在锁的保护下。 -
notify()
时机不当: 有时,开发者会先释放锁,然后才调用notify()
。这通常是没问题的,甚至在某些高性能场景下,可以减少被唤醒线程重新获取锁的竞争。但如果你的逻辑要求被唤醒的线程能够立即获取锁并处理数据,那么在持有锁的时候调用notify()
可能更直接。关键在于理解你的程序流和竞争条件。 - 死锁: 这是一个普遍的并发问题,与条件变量结合时也可能出现。例如,如果一个线程持有锁A,然后尝试等待条件变量(这会释放锁A),但另一个线程需要锁A才能修改条件并发出通知,这就可能导致死锁。注意锁











