【避免死锁】:std::condition_variable的高级用法及最佳实践
发布时间: 2024-10-20 13:25:52 阅读量: 45 订阅数: 21
![C++的std::condition_variable(条件变量)](https://help.autodesk.com/sfdcarticles/img/0EM3A000000ERoy)
# 1. std::condition_variable概述
`std::condition_variable` 是 C++11 引入的一种同步原语,主要用于在多线程环境中协调线程之间的同步和通信。它允许线程在某些条件成立之前进行阻塞,并在条件成立时由其他线程唤醒。这一机制对于实现生产者-消费者模式、任务等待、条件等待等场景至关重要。
在传统的多线程编程中,线程间的协作往往依赖于互斥锁(mutex)来保护共享资源,但仅凭互斥锁无法有效解决线程间的条件同步问题。这时,`std::condition_variable` 便发挥了其作用,能够与互斥锁一起,使线程在资源未就绪时睡眠,并在条件满足时被唤醒。
本章将为读者提供一个初步的概述,涵盖 `std::condition_variable` 的基础概念、用途和典型使用模式,为后面章节中深入讨论其工作机制和高级用法打下基础。
# 2. 深入理解std::condition_variable
## 2.1 条件变量的基本概念与工作原理
### 2.1.1 条件变量与互斥锁的配合
在多线程编程中,条件变量是同步机制的一种,它允许线程在某些条件不满足时挂起执行,直到其他线程改变了条件并发出通知。std::condition_variable与std::mutex一起工作,以确保线程安全的条件检查和等待。
互斥锁(mutex)用于同步访问共享资源,而条件变量用于在条件不满足时阻塞线程,并在条件满足时唤醒线程。为了正确使用条件变量,每个条件变量对象应当与一个互斥锁对象关联。这个互斥锁被用来保护那些决定条件是否成立的数据,确保在检查条件和等待条件时不会有其他线程修改这些数据。
下面是一个简单的例子,展示了条件变量和互斥锁的基本配合使用:
```cpp
#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
std::mutex mu;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lk(mu);
while (!ready) { // 1. 需要锁来保护ready变量
cv.wait(lk); // 2. 释放锁,线程挂起等待通知
}
std::cout << "Thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lk(mu);
ready = true;
cv.notify_all(); // 3. 唤醒所有等待的线程
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go();
for (auto& th : threads)
th.join();
return 0;
}
```
在这个例子中,`ready` 是被保护的共享变量,我们在检查它的状态之前必须获得锁。如果条件不满足(即 `ready` 为 `false`),线程会调用 `cv.wait(lk)`,该操作会释放锁并使线程等待。当另一个线程调用 `cv.notify_all()` 后,所有等待的线程会被唤醒,但在返回之前,它们会重新获取锁。
### 2.1.2 等待与通知机制详解
std::condition_variable 提供了两种等待操作:`wait` 和 `notify_one/notify_all`。等待操作允许一个或多个线程在某些条件尚未满足时挂起执行,而通知操作则可以唤醒那些因等待条件而挂起的线程。
`wait` 函数使得线程等待,直到它被 `notify_one` 或 `notify_all` 唤醒。当线程因为条件变量的 `wait` 操作而被阻塞时,它会自动释放已经持有的互斥锁,这样其他线程就可以改变条件并调用通知函数了。
通知函数 `notify_one` 唤醒一个正在等待当前条件变量的线程,而 `notify_all` 则唤醒所有等待的线程。但需要注意的是,唤醒操作并不保证哪个线程会首先响应,这依赖于操作系统的调度机制。
在某些情况下,条件变量的等待操作还可能因为虚假唤醒而返回,即使条件变量没有收到通知。因此,总是应当在 `wait` 函数返回后重新检查条件是否满足。
```cpp
cv.wait(lk, []{ return ready; }); // 使用lambda表达式作为条件
```
在这个例子中,`wait` 函数接受一个额外的参数,这是一个可调用对象(比如 lambda 表达式)。只有当这个函数返回 `true` 时,`wait` 函数才会解除线程的挂起状态。这种方式可以用来避免虚假唤醒。
## 2.2 std::condition_variable的成员函数
### 2.2.1 wait函数的不同重载版本分析
`wait` 函数有多种重载版本,每种版本都提供了不同的机制来处理线程的挂起和唤醒。
最基本的形式是 `wait(lock)`,它接受一个已经持有的互斥锁对象。当该函数被调用时,它会先释放锁,允许其他线程获取锁并修改条件变量保护的条件。然后,函数会挂起当前线程直到被其他线程通过 `notify_one` 或 `notify_all` 唤醒。当线程被唤醒后,`wait` 函数会再次获取锁,线程才继续执行。
另一种形式是 `wait(lock, predicate)`,它接受一个谓词函数作为额外参数。这种方式可以减少虚假唤醒的几率,因为 `wait` 函数会先检查谓词函数是否返回 `true`。如果谓词返回 `false`,则线程进入等待状态;如果返回 `true`,则线程继续执行。这避免了在没有实际更改条件的情况下被唤醒的无谓唤醒。
```cpp
std::unique_lock<std::mutex> lk(mu);
while (ready == false) {
cv.wait(lk); // 无谓唤醒的风险
}
// 改进后,使用谓词函数减少无谓唤醒
cv.wait(lk, []{ return ready; }); // 使用lambda表达式作为谓词
```
还有一种等待形式是 `wait_for` 和 `wait_until`,它们允许线程等待一个特定的时间。这可以防止线程永久等待,并且可以设置一个超时,超时后线程将自动解除阻塞并继续执行。
```cpp
std::this_thread::sleep_for(std::chrono::seconds(1));
cv.wait_for(lk, std::chrono::seconds(1)); // 等待最多1秒
```
### 2.2.2 notify_one与notify_all的行为差异
在多线程编程中,使用 `std::condition_variable` 时,开发者面临一个选择:使用 `notify_one` 还是 `notify_all` 来唤醒等待线程。这两种函数的行为存在明显的差异,了解它们的差异对于编写有效的多线程程序至关重要。
`notify_one` 函数会唤醒一个正在等待当前条件变量的线程。这意味着系统中只有一个等待线程会被唤醒,然后它会继续执行。在某些情况下,这种行为是合适的,尤其是当你知道只需要唤醒单个线程来处理一些工作。
```cpp
cv.notify_one(); // 唤醒单个等待线程
```
而 `notify_all` 函数则唤醒所有正在等待当前条件变量的线程。如果多个线程被唤醒,但只有一个线程能够执行它的任务,这可能会引起不必要的竞争条件。但是,如果任务可以由多个线程并行处理,或者有多个等待条件,那么 `notify_all` 将是更合适的选择。
```cpp
cv.notify_all(); // 唤醒所有等待线程
```
选择 `notify_one` 还是 `notify_all` 应当基于实际的程序逻辑和设计需求。通常,如果只有一个线程需要对条件变化做出响应,那么 `notify_one` 是一个不错的选择。如果任务可以由多个线程处理,或者多个线程需要同时响应条件变化,那么 `notify_all` 将是更恰当的选择。
## 2.3 条件变量在多线程中的协作模式
### 2.3.1 生产者-消费者模型
生产者-消费者问题是计算机科学中的一个经典问题,用来描述线程之间的协作。在这一问题中,生产者线程负责生成数据,而消费者线程则消费这些数据。条件变量在这个模型中扮演着协调生产者和消费者行为的重要角色。
生产者-消费者模型通常涉及到数据的缓冲区,生产者将数据放入缓冲区,而消费者从缓冲区中取出数据进行处理。使用互斥锁和条件变量可以防止缓冲区同时被生产者和消费者访问,从而避免竞争条件和数据损坏。
下面是一个使用条件变量实现的生产者-消费者模型的示例:
```cpp
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
std::queue<int> queue;
std::mutex mu;
std::condition_variable cv;
void producer(int value) {
std::unique_lock<std::mutex> lk(mu);
std::cout << "Producer produced " << value << '\n';
queue.push(value);
cv.notify_one(); // 通知消费者有新的数据
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lk(mu);
cv.wait(lk, []{ return !queue.empty(); }); // 等待队列非空
int value = queue.front();
queue.pop();
lk.unlock(); // 注意:在操作共享数据后,离开作用域时才解锁
std::cout << "Consumer consumed " << value << '\n';
}
}
int main() {
std::thread t1(consumer);
std::thread t2(producer, 1);
std::thread t3(producer, 2);
t2.join();
t3.join();
t1.join(); // 主线程等待消费者线程结束
return 0;
}
```
在这个例子中,生产者在生产数据后会通知消费者线程。消费者线程则在队列不为空的情况下等待。互斥锁用来保护队列,防止多个线程同时操作队列造成的数据竞争。
### 2.3.2 多生产者-多消费者模型
当有多于一个生产者和一个消费者时,情况会变得更加复杂。在这种情况下,我们不仅要保证生产者和消费者之间的同步,还要保证生产者之间和消费者之间不会发生冲突。
在多生产者-多消费者模型中,我们需要确保每个生产者都能够在不与其他生产者冲突的情况下向共享缓冲区中添加数据,同样,消费者也需要在不与其他消费者冲突的情况下从缓冲区中移除数据。
为了实现这一点,可以使用多个条件变量,每个条件变量对应一组生产者或消费者。这种方法可以减少不必要的唤醒,因为它允许更精确地控制哪个线程组应当被唤醒。
```cpp
// 假设有两个生产者和两个消费者
std::mutex mu;
std::condition_variable cv_empty, cv_full;
size_t empty = 10; // 缓冲区空槽位数量
size_t full = 0; // 缓冲区数据项数量
std::queue<int> queue;
void producer(int value) {
std::unique_lock<std::mutex> lk(mu);
cv_empty.wait(lk, []{ return empty > 0; }); // 等待空槽位
queue.push(value);
--e
```
0
0