condition_variable的wait函数工作原理
锁的配合wait必须接收参数,因为需要在阻塞时释放锁、唤醒时重新获取锁(不支持手动释放,无法配合)。原子操作:释放锁和进入阻塞是原子的,避免“唤醒丢失”。虚假唤醒处理:必须通过循环检查条件(带谓词的wait自动实现),否则可能因虚假唤醒导致错误。唤醒后竞争:被唤醒的线程需要重新竞争锁,因此wait返回后,共享资源可能已被其他线程修改(需重新检查条件)。理解wait的工作机制,是正确使用进行多线程同
std::condition_variable::wait
是条件变量的核心函数,用于让当前线程在某个条件未满足时进入阻塞状态,并在条件可能满足时被唤醒。其工作机制涉及锁的释放与重获取、阻塞与唤醒、虚假唤醒处理三个关键环节,下面详细解析:
一、wait 函数的两种重载形式
wait
有两种常用重载,核心逻辑一致,只是是否自动处理“条件检查”的区别:
// 形式1:不带条件谓词(Predicate)
void wait(std::unique_lock<std::mutex>& lock);
// 形式2:带条件谓词(推荐使用)
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
二、wait 函数的核心工作流程(以形式1为例)
wait
的核心作用是:释放锁 → 阻塞等待 → 被唤醒后重新获取锁。具体步骤如下:
-
释放锁(原子操作)
当线程调用wait(lock)
时,首先会自动释放lock
所持有的互斥锁(std::mutex
)。这一步是原子操作(不会被其他线程打断),确保释放锁和进入阻塞状态是连续的,避免“唤醒丢失”(即其他线程在当前线程释放锁前就发送了通知,导致通知被遗漏)。 -
阻塞等待通知
释放锁后,当前线程进入阻塞状态(不再占用CPU时间片),等待其他线程通过notify_one()
或notify_all()
发送通知。此时线程处于“等待状态”,不会参与锁竞争。 -
被唤醒后重新获取锁
当线程收到通知(或发生虚假唤醒)时,会从阻塞状态唤醒,并自动重新获取lock
所持有的互斥锁(此时需要和其他线程竞争锁,可能会阻塞等待直到成功获取)。 -
函数返回
成功获取锁后,wait
函数返回,线程可以继续执行,此时可以安全地访问被锁保护的共享资源。
三、为什么需要带条件谓词的重载(形式2)?
形式1的 wait
存在一个问题:线程可能在无明确通知的情况下被唤醒(即“虚假唤醒”,由操作系统调度机制导致)。此时如果直接继续执行,可能访问到不符合预期的共享资源(比如消费者被虚假唤醒时,队列仍然为空)。
带条件谓词的 wait(lock, pred)
本质是一个“循环检查”的封装,等价于:
while (!pred()) { // 循环检查条件,处理虚假唤醒
wait(lock); // 条件不满足时继续等待
}
pred
是一个返回bool
的函数/ lambda 表达式,用于检查“等待的条件是否成立”(例如队列是否非空)。- 只有当
pred()
返回true
时,wait
才会返回,确保线程继续执行时条件一定满足。
四、代码示例:直观理解 wait 的工作过程
以生产者-消费者模型中消费者的等待逻辑为例,展示 wait
的工作细节:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx); // 1. 获取锁
std::cout << "消费者:获取锁,准备等待\n";
// 带条件谓词的wait:等待队列非空
cv.wait(lock, []{
std::cout << "消费者:检查条件(队列是否非空)\n";
return !q.empty();
});
// 4. 成功获取锁且条件满足,消费数据
int data = q.front();
q.pop();
std::cout << "消费者:消费数据 " << data << "(队列大小:" << q.size() << ")\n";
lock.unlock(); // 手动解锁(可选)
if (data == -1) break; // 退出标志
}
}
void producer() {
for (int i = 0; i < 2; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟生产耗时
std::lock_guard<std::mutex> lock(mtx); // 获取锁
q.push(i);
std::cout << "生产者:生产数据 " << i << "(队列大小:" << q.size() << ")\n";
cv.notify_one(); // 通知消费者
}
// 发送退出标志
std::lock_guard<std::mutex> lock(mtx);
q.push(-1);
cv.notify_one();
}
int main() {
std::thread c(consumer);
std::thread p(producer);
c.join();
p.join();
return 0;
}
五、运行结果解析(关键步骤标注)
消费者:获取锁,准备等待
消费者:检查条件(队列是否非空) // 首次检查条件,队列空(pred返回false)
// 此时wait释放锁,并阻塞等待...
生产者:生产数据 0(队列大小:1) // 生产者获取锁,生产数据
生产者:发送通知(notify_one)
// 消费者被唤醒,重新竞争并获取锁...
消费者:检查条件(队列是否非空) // 再次检查条件,队列非空(pred返回true)
消费者:消费数据 0(队列大小:0) // wait返回,继续执行
消费者:获取锁,准备等待
消费者:检查条件(队列是否非空) // 条件不满足,再次释放锁阻塞...
生产者:生产数据 1(队列大小:1) // 生产者再次生产
生产者:发送通知
// 消费者被唤醒,重新获取锁...
消费者:检查条件(队列是否非空) // 条件满足
消费者:消费数据 1(队列大小:0)
// ...后续处理退出标志...
六、核心要点总结
-
锁的配合:
wait
必须接收std::unique_lock<std::mutex>
参数,因为需要在阻塞时释放锁、唤醒时重新获取锁(std::lock_guard
不支持手动释放,无法配合)。 -
原子操作:释放锁和进入阻塞是原子的,避免“唤醒丢失”。
-
虚假唤醒处理:必须通过循环检查条件(带谓词的
wait
自动实现),否则可能因虚假唤醒导致错误。 -
唤醒后竞争:被唤醒的线程需要重新竞争锁,因此
wait
返回后,共享资源可能已被其他线程修改(需重新检查条件)。
理解 wait
的工作机制,是正确使用 std::condition_variable
进行多线程同步的核心基础。
更多推荐
所有评论(0)