文章目录
前言
在并发编程中,线程同步是确保共享资源安全访问的关键。C++中的条件变量作为一种同步原语,允许线程在特定条件下等待或通知其他线程,从而协调线程的执行。本文将简要介绍C++条件变量的基本概念、使用方法和注意事项,并通过示例展示其在并发编程中的应用。通过学习本文,您将能够掌握C++条件变量的基本用法,提升程序的性能和可靠性。
一、条件变量
1、基本概念
条件变量是一个同步原语,它允许线程在一定条件下等待或通知其他线程。当某个条件不满足时,一个线程可以选择等待,而不是忙等待(即不断地检查条件是否满足)。当条件满足时,另一个线程会通知等待的线程,使其能够继续执行。
2、C++中的条件变量
在C++中,条件变量是通过
<condition_variable>
头文件提供的std::condition_variable
类实现的。这个类提供了两个主要的成员函数:wait()
和notify_one()
(或notify_all()
)。
wait()
: 使当前线程进入等待状态,直到另一个线程调用notify_one()
或notify_all()
。在等待期间,当前线程会释放它持有的所有锁,从而允许其他线程访问共享资源。当收到通知并重新获得锁后,线程会退出等待状态并继续执行。notify_one()
: 唤醒一个正在等待的线程。如果有多个线程在等待,则只有一个线程会被唤醒。notify_all()
: 唤醒所有正在等待的线程。
3、使用条件变量的基本步骤
- 初始化条件变量和互斥锁:在使用条件变量之前,需要先创建一个
std::condition_variable
对象和一个std::mutex
对象。互斥锁用于保护共享资源,防止多个线程同时访问。 - 等待条件满足:在需要等待的线程中,首先锁定互斥锁,然后调用条件变量的
wait()
函数。这将释放互斥锁并使线程进入等待状态。 - 修改条件并通知:当条件发生变化时(通常是由另一个线程修改共享资源导致的),拥有互斥锁的线程应该调用
notify_one()
或notify_all()
来唤醒等待的线程。 - 继续执行:被唤醒的线程在重新获得互斥锁后会退出等待状态,并继续执行。
4、注意事项
- 避免虚假唤醒:条件变量可能会导致虚假唤醒,即在没有调用
notify_one()
或notify_all()
的情况下,线程被唤醒。因此,在wait()
返回后,应重新检查条件是否真正满足。
while (!(xxx条件) )
{
//虚假唤醒发生,由于while循环,再次检查条件是否满足,
//否则继续等待,解决虚假唤醒
wait();
}
- 正确管理锁:条件变量的
wait()
函数需要一个std::unique_lock
对象作为参数。std::unique_lock
是一个更加灵活的锁对象,可以在不同的作用域内自动管理锁的获取和释放。 - 正确处理异常:在锁定互斥锁和调用条件变量的过程中,如果发生异常,可能会导致锁没有被正确释放。因此,建议使用RAII(资源获取即初始化)技术来管理锁的生命周期,确保在异常发生时也能正确释放锁。
5、问题剖析
5.1、为什么只能使用std::unique_lock管理锁?
使用
std::unique_lock
而不是std::lock_guard
来管理与条件变量配合使用的互斥锁的原因主要有以下几点:
- 解锁再休眠:当线程调用
wait()
函数时,std::unique_lock
会自动释放互斥锁,让其他线程有机会获取锁并修改共享资源。然后当前线程进入休眠状态,直到收到通知或满足条件后再次自动获取锁。 - 灵活性和控制:
std::unique_lock
提供了更多的灵活性,包括延迟锁定、可重入锁定以及支持条件变量等待。它还允许在等待过程中使用timeout
或try_lock
等高级功能,这些是std::lock_guard
所不具备的。
总的来说,由于
std::unique_lock
具有在等待条件变量时释放锁的能力,并且在重新唤醒时能自动恢复锁定状态,它成为了与条件变量配合使用时的首选工具。而std::lock_guard
虽然简单易用,但由于其设计初衷是用于保护简单的代码块,并在异常安全方面提供保障,因此在需要更复杂同步逻辑的条件变量场景中不太适用。
5.2、notify_one()与notify_all()的主要区别?
notify_one()
和notify_all()
都是C++条件变量中用于唤醒等待的线程的方法,但它们在唤醒方式和使用场景上存在差异。具体分析如下:
- 唤醒方式:
notify_one()
只唤醒等待队列中的一个线程,而notify_all()
会唤醒所有等待在条件变量上的线程。 - 使用场景:如果多个线程在等待同一个条件,且只要一个线程处理就可以满足条件,那么
notify_one()
更为合适。它可以减少线程之间的竞争,提高程序的效率。相反,当条件发生变化,需要所有等待的线程都知晓并进行处理时,应该使用notify_all()
。这通常用于状态变化需要通知所有线程的场景。
总的来说,选择
notify_one()
还是notify_all()
取决于具体的同步需求和性能考虑。在不需要所有线程都被唤醒的情况下,使用notify_one()
可以提高效率,而在需要广播状态变化时,使用notify_all()
确保所有线程都能响应条件的变化。
6、使用条件变量的生产者-消费者模型示例
下面是一个使用C++条件变量的生产者-消费者模型示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false;
void producer(int id) {
for (int i = 0; i < 5; ++i) {
std::unique_lock<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "Producer " << id << " produced " << i << std::endl;
lock.unlock();
cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
finished = true;
cv.notify_one();
}
void consumer() {
while (!finished) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{return !data_queue.empty() || finished;});
if (!data_queue.empty()) {
int data = data_queue.front();
data_queue.pop();
std::cout << "Consumer consumed " << data << std::endl;
}
}
}
int main() {
std::thread producer1(producer, 1);
std::thread producer2(producer, 2);
std::thread consumer1(consumer);
producer1.join();
producer2.join();
consumer1.join();
return 0;
}
在这个示例中,我们定义了两个生产者线程和一个消费者线程。生产者线程负责生产数据,并将数据放入队列中;消费者线程负责从队列中取出数据并消费。通过使用条件变量,我们可以实现生产者和消费者之间的同步关系。当队列为空时,消费者线程会等待生产者线程生产数据;当队列中有数据时,消费者线程会被唤醒并消费数据。
7、总结
C++中的条件变量是实现线程同步的强大工具。通过合理使用条件变量和互斥锁,我们可以有效地控制多个线程对共享资源的访问,确保数据的一致性和正确性。然而,在使用条件变量时,我们也需要注意避免虚假唤醒、死锁和异常处理等问题。