在这里插入图片描述

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》


条件变量 (<condition_variable>)

如果说 std::mutex 是用来保护共享数据不被同时修改,那么 std::condition_variable 就是用来让线程在特定条件不满足时高效地等待,并在条件满足时被唤醒

组件 描述
std::condition_variable 用于线程间的等待和通知。一个线程可以等待某个条件成立,然后进入阻塞状态;另一个线程可以在条件成立时通知等待的线程。
wait() 线程调用 wait() 时会原子性地释放互斥量并进入阻塞状态。
notify_one() 唤醒一个等待中的线程。
notify_all() 唤醒所有等待中的线程。

💡 应用场景: 典型的生产者-消费者模型。当队列为空时,消费者等待;当生产者放入数据时,通知消费者。

1. 基本概述

1.1 核心概念

💡 条件变量用于解决以下问题

问题: 线程 A 需要等待某个共享状态(如队列中有数据、任务完成)发生改变后才能继续执行。如果线程 A 只是持续占用 CPU 资源去检查状态(称为忙等待/轮询),会导致巨大的资源浪费。

解决方案: 使用条件变量,线程 A 可以在条件不满足时释放锁并进入睡眠状态(不占用 CPU),直到另一个线程 B 改变状态并通知它。

条件变量总是与以下三个组件一起工作:

  1. 共享数据: 需要线程间同步的状态变量(如 bool 标志、队列)。
  2. 互斥量 (std::mutex): 保护共享数据不被同时访问。
  3. 条件变量 (std::condition_variable): 用于线程的等待和通知。

1.2 成员函数

A. 等待 (Waiting)

函数 作用 描述
wait(Lock& lock) 阻塞等待 原子性地执行三步操作: 1. 释放传入的锁(必须是 std::unique_lock)。 2. 阻塞当前线程,将其加入等待队列。 3. 被唤醒后,重新获取锁,并继续执行。
wait(Lock& lock, Predicate pred) 带谓词的等待 推荐用法。 在进入等待之前,先检查 pred(一个可调用对象,返回 bool)。 只有当 pred 返回 false 时才进入等待。 被唤醒后,会自动检查 pred,如果仍为 false,则回到等待状态(处理虚假唤醒)。

B. 通知 (Notifying)

函数 作用 描述
notify_one() 唤醒一个 唤醒一个等待在该条件变量上的线程。哪个线程被唤醒取决于操作系统调度器。
notify_all() 唤醒全部 唤醒所有等待在该条件变量上的线程。

1.3 为什么必须使用 std::unique_lock?

条件变量的 wait() 函数在内部需要原子性地完成“释放锁”和“进入等待”两个操作。

  • std::lock_guard 不提供手动释放锁的功能。
  • 只有 std::unique_lock 提供了必要的灵活性(可以被手动释放,并支持移动),能够满足 std::condition_variable::wait 的要求。因此,std::unique_lock 是与 std::condition_variable 配合的唯一合法锁类型。

2. 示例

2.1 基本使用
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void Print(int id) {
	std::unique_lock<std::mutex> lck(mtx);
	while (!ready) cv.wait(lck);
	std::cout << "Thread: " << id << "\n";
}

void go() {
	std::unique_lock<std::mutex> lck(mtx);
	ready = true;

	// 通知所有阻塞在条件变量上的线程
	cv.notify_all();
}

int main() {
	std::thread threads[10];
	for (int i = 0; i < 10; i++) {
		threads[i] = std::thread(Print, i);
	}
	std::this_thread::sleep_for(std::chrono::microseconds(100));
	go();
	for (auto& t : threads) t.join();
	return 0;
}
2.2 两线程交替打印奇偶数
int main() {
	std::mutex mtx;
	std::condition_variable cv;
	bool flag = true;

	std::thread t1([&]() {
		for (int i = 0; i < 10; i += 2) {
			std::unique_lock<std::mutex> lck(mtx);
			while (!flag) cv.wait(lck);
			std::cout << i << " ";
			flag = false;
			cv.notify_one();
		}
	});

	std::thread t2([&]() {
		for (int i = 1; i < 10; i += 2) {
			std::unique_lock<std::mutex> lck(mtx);
			while (flag) cv.wait(lck);
			std::cout << i << " ";
			flag = true;
			cv.notify_one();
		}
	});

	t1.join();
	t2.join();

	return 0;
}
2.3 生产消费者模型
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx; // 1. 互斥量:保护共享队列
std::condition_variable cv; // 2. 条件变量:用于通知
std::queue<int> data_queue; // 3. 共享数据:消息队列

// 消费者线程 (等待数据)
void consumer() {
    for (int i = 0; i < 5; ++i) {
        // 1. 必须使用 unique_lock
        std::unique_lock<std::mutex> lock(mtx);

        // 2. wait():等待谓词条件 data_queue.empty() == false 满足
        //    如果队列为空,wait() 会自动释放 mtx 并阻塞当前线程
        //    当被通知并唤醒后,wait() 会重新获取 mtx
        cv.wait(lock, []{ return !data_queue.empty(); });
        
        // 条件满足,取出数据
        int data = data_queue.front();
        data_queue.pop();
        std::cout << "Consumer consumed: " << data << "\n";
        
        // 3. 离开作用域,unique_lock 自动解锁
    }
}

// 生产者线程 (产生数据并通知)
void producer() {
    for (int i = 0; i < 5; ++i) {
        // 1. 锁定互斥量以修改共享数据
        std::unique_lock<std::mutex> lock(mtx);
        
        // 放入数据
        data_queue.push(i);
        std::cout << "Producer produced: " << i << "\n";

        // 2. 释放锁 (在通知之前释放可以提高效率,但也可以在 notify 之后由 lock 析构自动释放)
        // lock.unlock(); 

        // 3. 通知等待中的线程:数据已就绪
        cv.notify_one();         
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons(consumer);

    prod.join();
    cons.join();
    
    return 0;
}

3. 虚假唤醒 (Spurious Wakeup)

虚假唤醒是指等待中的线程在没有收到 notify_one()notify_all() 通知的情况下,或者在条件尚未真正满足的情况下,被操作系统唤醒。

尽管这种情况很少见,但它是标准允许的。为了程序的健壮性,必须处理虚假唤醒:

  • 解决方法: 总是使用带谓词 (Predicate)wait() 重载版本:

    cv.wait(lock, []{ return condition_is_met; });
    

    这个版本会在唤醒后自动检查条件是否真正满足。如果不满足,线程会再次进入等待状态,从而避免了虚假唤醒带来的逻辑错误。

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【C++】的内容,请持续关注我 !!

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐