一、引言
在 C++ 编程的世界里,多线程编程可是个非常重要的技术。它能让程序同时处理多个任务,大大提高程序的性能和响应速度。不过,多线程编程也带来了很多挑战,比如线程之间的同步问题。今天咱们就来聊聊其中一个很有用的工具——条件变量,看看它在 C++ 多线程编程里该怎么正确使用。
二、条件变量是什么
条件变量就像是一个协调员,它能让线程在某个条件满足的时候才继续执行,不满足的时候就先等着。打个比方,有两个线程,一个负责生产数据,一个负责消费数据。消费者线程得等生产者线程生产出数据之后才能开始工作,这时候条件变量就能发挥作用了,它可以让消费者线程在没有数据的时候先暂停,等有数据了再接着干。
在 C++ 里,条件变量定义在 <condition_variable> 头文件里,主要有两个类型:std::condition_variable 和 std::condition_variable_any。std::condition_variable 只能和 std::unique_lock<std::mutex> 一起用,而 std::condition_variable_any 可以和任何满足互斥锁要求的锁类型一起用。
三、条件变量的基本用法
下面咱们来看一个简单的例子,看看条件变量是怎么用的:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 生产者线程函数
void producer() {
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 设置标志位,表示数据准备好了
}
cv.notify_one(); // 通知一个等待的线程
}
// 消费者线程函数
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
// 等待条件变量通知,并且检查标志位
cv.wait(lock, []{ return ready; });
std::cout << "Consumer: Data is ready and processing..." << std::endl;
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
在这个例子里,生产者线程先睡 2 秒钟,模拟生产数据的过程。然后它设置 ready 标志位为 true,表示数据准备好了,接着调用 cv.notify_one() 通知一个等待的线程。消费者线程用 std::unique_lock 锁住互斥锁,然后调用 cv.wait() 等待条件变量的通知。wait() 函数会自动释放锁,让其他线程可以访问共享资源。当收到通知并且 ready 标志位为 true 时,wait() 函数会重新获取锁,然后继续执行。
四、条件变量的应用场景
1. 生产者 - 消费者模型
这是条件变量最常见的应用场景之一。就像上面的例子一样,生产者线程负责生产数据,消费者线程负责消费数据。生产者生产完数据后通知消费者,消费者在没有数据的时候就等待。这样可以避免消费者线程一直循环检查是否有数据,节省了 CPU 资源。
2. 线程池
在线程池里,主线程会创建一定数量的工作线程。当有任务到来时,主线程把任务放到任务队列里,然后通知一个空闲的工作线程来处理。工作线程在没有任务的时候就等待,有任务了就开始工作。
下面是一个简单的线程池示例:
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <atomic>
class ThreadPool {
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queueMutex);
// 等待任务队列有任务或者线程池停止
this->condition.wait(lock, [this] { return this->stop ||!this->tasks.empty(); });
if (this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task(); // 执行任务
}
});
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queueMutex);
stop = true;
}
condition.notify_all(); // 通知所有线程停止
for (std::thread &worker : workers) {
worker.join();
}
}
template<class F>
void enqueue(F &&f) {
{
std::unique_lock<std::mutex> lock(queueMutex);
if (stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace(std::forward<F>(f));
}
condition.notify_one(); // 通知一个线程有新任务
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
std::atomic<bool> stop;
};
// 示例任务函数
void task() {
std::cout << "Task is being executed." << std::endl;
}
int main() {
ThreadPool pool(4);
// 向线程池添加任务
for (int i = 0; i < 10; ++i) {
pool.enqueue(task);
}
return 0;
}
五、条件变量的技术优缺点
优点
- 节省 CPU 资源:条件变量可以让线程在不满足条件的时候进入等待状态,避免了线程的空转,节省了 CPU 资源。
- 线程同步:它能很好地实现线程之间的同步,确保线程在合适的时机执行相应的操作。
缺点
- 使用复杂:条件变量的使用需要和互斥锁配合,而且要注意锁的获取和释放顺序,容易出错。
- 虚假唤醒:有时候线程会在没有收到通知的情况下从
wait()函数返回,这就是虚假唤醒。所以在使用wait()函数时,一定要检查条件是否真的满足。
六、条件变量的注意事项
1. 虚假唤醒
正如前面提到的,虚假唤醒是一个需要注意的问题。为了避免虚假唤醒,我们在使用 wait() 函数时,一定要传入一个条件判断函数。比如:
cv.wait(lock, []{ return ready; });
这样即使发生了虚假唤醒,线程也会检查 ready 标志位,如果条件不满足,它会继续等待。
2. 锁的使用
在使用条件变量时,一定要正确使用互斥锁。wait() 函数会自动释放锁,当收到通知时会重新获取锁。所以在调用 wait() 函数之前,一定要先获取锁。
3. 通知的顺序
在通知线程时,要注意通知的顺序。如果在设置条件之前就通知,可能会导致等待的线程错过通知。所以一般要先设置条件,再通知。
七、文章总结
条件变量是 C++ 多线程编程里一个非常有用的工具,它能很好地解决线程之间的同步问题。通过合理使用条件变量,我们可以实现高效的生产者 - 消费者模型、线程池等。不过,使用条件变量也有一些需要注意的地方,比如虚假唤醒、锁的使用和通知的顺序等。只要我们掌握了这些要点,就能在多线程编程中正确使用条件变量,提高程序的性能和稳定性。
评论