【C++现代#4】C++11并发支持库 --条件变量 <condition_variable>

📃个人主页: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 改变状态并通知它。
条件变量总是与以下三个组件一起工作:
- 共享数据: 需要线程间同步的状态变量(如
bool标志、队列)。 - 互斥量 (
std::mutex): 保护共享数据不被同时访问。 - 条件变量 (
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++】的内容,请持续关注我 !!

更多推荐


所有评论(0)