C++ 标准库 condition_variable
下面对 C++11 起 <condition_variable>
头文件中提供的条件变量及其配套同步机制做一次系统、深入的梳理,包括类型定义、主要接口、使用范式、常见陷阱与实践建议。
一、概述
- 条件变量(Condition Variable)是一种线程同步原语,用于在线程间等待某个条件成立,并在条件发生变化时通知一个或多个等待线程继续执行。
- 与互斥量(
<mutex>
)配合使用,可避免忙等(busy-wait),在高并发情况下大幅降低 CPU 占用。 <condition_variable>
中主要提供两种类型:std::condition_variable
:只能与std::unique_lock<std::mutex>
配合使用std::condition_variable_any
:可与任意符合 BasicLockable 的锁类型配合
二、主要类型和成员
1. std::condition_variable
class condition_variable {
public:
condition_variable();
~condition_variable();
// 禁止拷贝,可移动(C++20 起)
condition_variable(const condition_variable&) = delete;
condition_variable& operator=(const condition_variable&) = delete;
// 唤醒一个等待线程
void notify_one() noexcept;
// 唤醒所有等待线程
void notify_all() noexcept;
// 等待(会自动解锁并阻塞,返回前重新加锁)
void wait(std::unique_lock<std::mutex>& lock);
// 带谓词重载:若谓词 false 则继续等待
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
// 带时限等待,返回 false 表示超时
template<class Rep, class Period>
std::cv_status wait_for(
std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep,Period>& rel_time);
template<class Rep, class Period, class Predicate>
bool wait_for(
std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep,Period>& rel_time,
Predicate pred);
// 绝对时刻等待,返回 false 表示超时
template<class Clock, class Duration>
std::cv_status wait_until(
std::unique_lock<std::mutex>& lock,
const std::chrono::time_point<Clock,Duration>& abs_time);
template<class Clock, class Duration, class Predicate>
bool wait_until(
std::unique_lock<std::mutex>& lock,
const std::chrono::time_point<Clock,Duration>& abs_time,
Predicate pred);
};
wait(lock)
:- 解锁
lock
(当前线程可释放对共享数据的独占访问); - 阻塞当前线程,直到被
notify
唤醒; - 返回前重新对
lock
加锁;
- 解锁
- 带谓词重载:内部循环调用
wait
,直到谓词返回true
,避免伪唤醒(spurious wakeup)问题。 - 时限版本:可指定相对时长或绝对时刻等待,超时则返回
false
(或std::cv_status::timeout
)。
2. std::condition_variable_any
- 接口与
condition_variable
相同,但模板化到任意“BasicLockable”对象。 - 代价略大,通常在需要用自定义锁(如读写锁、spinlock)时使用。
三、基本使用范式
以生产者—消费者队列为例:
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lk(mtx);
data_queue.push(i);
}
cv.notify_one(); // 通知一个消费者
}
{
std::lock_guard<std::mutex> lk(mtx);
finished = true;
}
cv.notify_all(); // 通知所有消费者结束
}
void consumer(int id) {
std::unique_lock<std::mutex> lk(mtx);
while (true) {
cv.wait(lk, []{
return !data_queue.empty() || finished;
});
if (!data_queue.empty()) {
int value = data_queue.front();
data_queue.pop();
lk.unlock();
// 处理数据
std::cout << "Consumer " << id << " got " << value << "\n";
lk.lock();
} else if (finished) {
break;
}
}
}
int main() {
std::thread p(producer);
std::thread c1(consumer, 1), c2(consumer, 2);
p.join();
c1.join();
c2.join();
}
- 步骤:
- 生产者在互斥保护下修改共享队列,随后
notify_one()
或notify_all()
; - 消费者通过
wait(lock, pred)
等待队列非空或生产结束; - 唤醒后处理数据,再次进入等待或退出循环。
- 生产者在互斥保护下修改共享队列,随后
四、常见问题与陷阱
- 伪唤醒
- 操作系统可能无故唤醒等待线程,必须在
while(!condition)
循环中调用wait
,或使用带谓词重载的wait(lock, pred)
。
- 操作系统可能无故唤醒等待线程,必须在
- 丢失通知
- 如果在调用
wait
之前就已经发生了notify
,而此时线程尚未进入等待,会错过通知。 - 必须在检查条件后再等待:
std::unique_lock<std::mutex> lk(mtx); while (!ready) // 先检查 cv.wait(lk);
- 如果在调用
- 死锁
- 在持有锁期间调用回调函数或用户代码可能重新尝试锁定同一互斥量,应避免在
wait
前后或通知中调用可能再次加锁的操作。
- 在持有锁期间调用回调函数或用户代码可能重新尝试锁定同一互斥量,应避免在
- 通知效率
notify_one()
唤醒一个线程,适用于单一消费者;多消费者场景应视具体,避免notify_all()
每次都唤醒所有线程造成“惊群”效应。
- 锁释放时机
wait
释放锁后才阻塞;超时版本若返回timeout
,锁已重新获得,调用者要立刻检查条件再决定下一步。
五、实践建议
- 使用带谓词的
wait
- 省去手写循环,避免因忘记循环判断导致错误。
- 最小化临界区
- 只在检查条件与修改共享数据时持锁,处理数据时应尽早释放锁,降低竞争。
- 合理选择
notify_one
vsnotify_all
- 单一资源竞争时优先
notify_one()
;批量状态改变(如“结束”标志)时用notify_all()
。
- 单一资源竞争时优先
- 封装同步逻辑
- 将条件变量、互斥量与状态一起封装到类中,通过成员函数对外提供线程安全接口,避免外部误用。
- 考虑超时机制
- 长时间等待可能导致单个线程“挂死”,建议在适当场景下使用
wait_for
或wait_until
并结合超时处理。
- 长时间等待可能导致单个线程“挂死”,建议在适当场景下使用
- 调试工具
- 可借助 ThreadSanitizer(TSan)等工具检测数据竞争和死锁,或在线程启动/唤醒处添加日志以跟踪执行流。
通过以上对 <condition_variable>
中两种条件变量类型、等待与唤醒机制、使用示例、常见误区及优化建议的全面梳理,希望能帮助你在多线程场景中高效、正确地使用条件变量实现线程间协作。祝编码顺利!