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 的核心作用是:释放锁 → 阻塞等待 → 被唤醒后重新获取锁。具体步骤如下:

  1. 释放锁(原子操作)
    当线程调用 wait(lock) 时,首先会自动释放 lock 所持有的互斥锁(std::mutex)。这一步是原子操作(不会被其他线程打断),确保释放锁和进入阻塞状态是连续的,避免“唤醒丢失”(即其他线程在当前线程释放锁前就发送了通知,导致通知被遗漏)。

  2. 阻塞等待通知
    释放锁后,当前线程进入阻塞状态(不再占用CPU时间片),等待其他线程通过 notify_one()notify_all() 发送通知。此时线程处于“等待状态”,不会参与锁竞争。

  3. 被唤醒后重新获取锁
    当线程收到通知(或发生虚假唤醒)时,会从阻塞状态唤醒,并自动重新获取 lock 所持有的互斥锁(此时需要和其他线程竞争锁,可能会阻塞等待直到成功获取)。

  4. 函数返回
    成功获取锁后,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)

// ...后续处理退出标志...

六、核心要点总结

  1. 锁的配合wait 必须接收 std::unique_lock<std::mutex> 参数,因为需要在阻塞时释放锁、唤醒时重新获取锁(std::lock_guard 不支持手动释放,无法配合)。

  2. 原子操作:释放锁和进入阻塞是原子的,避免“唤醒丢失”。

  3. 虚假唤醒处理:必须通过循环检查条件(带谓词的 wait 自动实现),否则可能因虚假唤醒导致错误。

  4. 唤醒后竞争:被唤醒的线程需要重新竞争锁,因此 wait 返回后,共享资源可能已被其他线程修改(需重新检查条件)。

理解 wait 的工作机制,是正确使用 std::condition_variable 进行多线程同步的核心基础。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐