C++ 11 中 condition_variable 的探索与实践
文章目录
- 一、条件变量的基本概念
- 1.1 条件变量的定义
- 1.2 条件变量与互斥锁的配合
- 二、条件变量的基本用法
- 2.1 常见的操作
- 2.2 示例:生产者 - 消费者模型
- 代码说明
- 三、深入理解条件变量
- 3.1 条件变量的底层实现
- 3.2 条件变量与忙等待的对比
- 3.3 提升性能的注意事项
- 避免虚假唤醒
- 最小化锁的持有时间
- 四、条件变量的应用场景
- 4.1 生产者 - 消费者模型
- 4.2 读者 - 写者模型
- 4.3 线程池
- 五、条件变量的相关类和成员函数
- 5.1 相关类
- `std::condition_variable`
- `std::condition_variable_any`
- 5.2 相关成员函数
- `wait()`
- `wait_for()`
- `wait_until()`
- `notify_one()`
- `notify_all()`
- 六、条件变量的示例代码分析
- 6.1 生产者 - 消费者模型示例
- 代码分析
- 6.2 线程交替打印示例
- 代码分析
- 七、总结
- 八、条件变量的示意图
在现代计算机编程的广阔天地里,多核处理器的普及宛如一阵春风,吹开了多线程编程的繁花。多线程编程,这一构建高性能应用程序的利器,逐渐成为了开发者们手中的法宝。然而,在多线程程序的世界里,线程间的同步和通信就像是一座难以跨越的大山,横亘在开发者面前。C++ 标准库自 C++11 开始,正式引入了
<condition_variable>
(条件变量)这一工具,如同一把利剑,为开发者们劈开了这座大山,提供了一种高效、灵活的线程同步机制。
一、条件变量的基本概念
1.1 条件变量的定义
条件变量 (Condition Variable) 是一种线程同步机制,它就像是一位公正的裁判,用于在线程之间等待某个条件的成立,并通知其他线程这一情况。它通常与互斥锁 (std::mutex
) 配合使用,就像是一对默契的搭档,以保证线程间的同步和数据访问的安全性。
简单来说,条件变量可以让一个线程阻塞等待特定条件的成立,当条件满足时,其他线程可以通过通知 (notify_one
或 notify_all
) 唤醒等待的线程。在 C++ 标准库中,条件变量通过以下两个类实现:
std::condition_variable
:适用于普通线程同步,就像是一把精准的手术刀,专门用于特定场景。std::condition_variable_any
:通用版本,支持与任意的锁类型配合使用,如同一个万能的工具箱,适用于各种复杂情况。
1.2 条件变量与互斥锁的配合
条件变量和互斥锁的配合使用是线程同步的关键。互斥锁用于保护共享资源,防止多个线程同时访问导致数据不一致。而条件变量则用于在线程之间传递信号,通知线程何时可以继续执行。
当一个线程需要等待某个条件满足时,它会先获取互斥锁,然后检查条件是否满足。如果条件不满足,线程会调用条件变量的 wait
函数,释放互斥锁并进入等待状态。当其他线程修改了共享变量并满足条件时,它会调用条件变量的 notify_one
或 notify_all
函数,唤醒等待的线程。被唤醒的线程会重新获取互斥锁,继续执行。
这种配合方式可以避免线程的忙等待,提高程序的效率。例如,在生产者 - 消费者模型中,生产者线程会等待缓冲区中有足够空间后再生产新数据,消费者线程会等待缓冲区非空,然后从中取出数据。通过条件变量和互斥锁的配合,可以实现线程间的高效同步。
二、条件变量的基本用法
2.1 常见的操作
条件变量提供了以下几个核心操作:
wait
:阻塞当前线程,直到条件满足或被其他线程通知。就像是一个沉睡的巨人,等待着被唤醒的那一刻。notify_one
:通知一个等待中的线程。如同在黑暗中点亮一盏明灯,唤醒一个沉睡的灵魂。notify_all
:通知所有等待中的线程。仿佛是一声嘹亮的号角,唤醒所有沉睡的勇士。
2.2 示例:生产者 - 消费者模型
条件变量的一个经典应用场景是生产者 - 消费者模型。以下是一个简单的例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>std::queue<int> buffer;
const unsigned int MAX_BUFFER_SIZE = 10;
std::mutex mtx;
std::condition_variable cv;void producer(int id) { for (int i = 0; i < 20; ++i) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return buffer.size() < MAX_BUFFER_SIZE; }); buffer.push(i); std::cout << "Producer " << id << " produced: " << i << std::endl; lock.unlock(); cv.notify_all(); }
}void consumer(int id) { for (int i = 0; i < 20; ++i) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return !buffer.empty(); }); int item = buffer.front(); buffer.pop(); std::cout << "Consumer " << id << " consumed: " << item << std::endl; lock.unlock(); cv.notify_all(); }
}int main() { std::thread prod1(producer, 1); std::thread prod2(producer, 2); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod1.join(); prod2.join(); cons1.join(); cons2.join(); return 0;
}
代码说明
- 生产者线程会等待缓冲区中有足够空间后再生产新数据。当缓冲区已满时,线程会调用
cv.wait
函数,释放互斥锁并进入等待状态。当缓冲区有空闲空间时,其他线程会调用cv.notify_all
函数,唤醒等待的生产者线程。 - 消费者线程会等待缓冲区非空,然后从中取出数据。当缓冲区为空时,线程会调用
cv.wait
函数,释放互斥锁并进入等待状态。当缓冲区有数据时,其他线程会调用cv.notify_all
函数,唤醒等待的消费者线程。 cv.wait
用于阻塞当前线程,直到条件满足为止。在调用cv.wait
函数时,线程会自动释放互斥锁,避免死锁。cv.notify_all
唤醒所有等待中的线程。当条件满足时,线程会调用cv.notify_all
函数,通知所有等待的线程条件已经满足。
通过这个例子,我们可以看到条件变量在生产者 - 消费者模型中的重要作用。它可以实现线程间的高效同步,避免线程的忙等待,提高程序的性能。
三、深入理解条件变量
3.1 条件变量的底层实现
条件变量的底层实现通常依赖于操作系统提供的同步原语,例如 POSIX 下的 pthread_cond_t
或 Windows API 的 CONDITION_VARIABLE
。C++ 标准库中的条件变量通过互斥锁 (std::mutex
) 和条件变量本身相互结合,形成了一种高效的线程同步机制。
核心机制包括:
- 等待:线程调用
wait
时,会先解锁关联的互斥锁并进入休眠状态。这样可以防止因线程阻塞导致的死锁。就像是一个聪明的旅行者,在等待的过程中不会占用太多资源。 - 通知:调用
notify_one
或notify_all
时,会唤醒一个或多个等待中的线程,并重新尝试获取互斥锁。如同一位使者,传递重要的信息,唤醒沉睡的人们。
3.2 条件变量与忙等待的对比
在没有条件变量的情况下,线程通常会采用“忙等待”的方式检查条件是否成立。这种方法会导致 CPU 资源的浪费。例如:
while (!condition) { // 忙等待,浪费 CPU 资源
}
相比之下,条件变量的优点在于:
- 高效:线程可以在等待时进入休眠状态,而不是占用 CPU。就像是一个懂得休息的运动员,在等待的过程中保存体力。
- 简洁:无需手动管理线程间的通信逻辑。如同一个自动化的生产线,减少了人工干预。
3.3 提升性能的注意事项
避免虚假唤醒
条件变量的 wait
函数可能会因虚假唤醒 (spurious wakeup) 而提前返回。因此,建议将 wait
的调用写成以下形式:
cv.wait(lock, [] { return condition; });
通过传入一个谓词函数,可以确保线程只有在条件成立时才会继续执行。这就像是一个严格的门卫,只有满足条件的人才能进入。
最小化锁的持有时间
条件变量的通知操作 (notify_one
或 notify_all
) 不需要持有锁。因此,在修改共享变量并通知等待的线程时,应该尽量减少锁的持有时间,避免其他线程长时间等待。例如:
{std::lock_guard<std::mutex> lock(mtx);// 修改共享变量
}
cv.notify_one();
这样可以提高程序的并发性能,让更多的线程能够同时执行。
四、条件变量的应用场景
4.1 生产者 - 消费者模型
生产者 - 消费者模型是条件变量的经典应用场景。在这个模型中,生产者线程负责生产数据,消费者线程负责消费数据。通过条件变量和互斥锁的配合,可以实现线程间的高效同步,避免数据竞争和死锁。
4.2 读者 - 写者模型
读者 - 写者模型是另一个常见的应用场景。在这个模型中,读者线程可以同时读取共享资源,而写者线程需要独占访问共享资源。通过条件变量和互斥锁的配合,可以实现读者和写者之间的同步,避免数据不一致。
4.3 线程池
线程池是一种常见的并发编程模型,用于管理和复用线程。在线程池中,线程会等待任务的到来,当有任务时,线程会被唤醒并执行任务。通过条件变量和互斥锁的配合,可以实现线程池的高效管理,提高程序的性能。
五、条件变量的相关类和成员函数
5.1 相关类
std::condition_variable
std::condition_variable
是一个类,用于实现条件变量的基本功能。它只能与 std::unique_lock<std::mutex>
一起使用。
std::condition_variable_any
std::condition_variable_any
是一个更通用的条件变量类,它可以与任何满足可锁定 (Lockable) 要求的锁类型一起使用,而不仅仅局限于 std::unique_lock<std::mutex>
。
5.2 相关成员函数
wait()
使当前线程阻塞,直到收到通知或发生虚假唤醒。调用该函数时,线程会释放其所持有的锁,进入等待状态。当收到通知后,线程会重新获取锁并继续执行。
有两个重载版本如下:
void wait( std::unique_lock<std::mutex>& lock );
:无条件等待。template< class Predicate > void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
:等待直到pred()
返回true
,可以避免虚假唤醒。
wait_for()
使当前线程阻塞,直到收到通知、发生虚假唤醒或达到指定的超时时间。返回值表示线程被唤醒的原因。
函数原型:template< class Rep, class Period > std::cv_status wait_for( std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep,Period>& rel_time );
wait_until()
使当前线程阻塞,直到收到通知、发生虚假唤醒或到达指定的时间点。返回值表示线程被唤醒的原因。
函数原型:template< class Clock, class Duration > std::cv_status wait_until( std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock,Duration>& timeout_time );
notify_one()
唤醒一个等待在该条件变量上的线程。如果没有线程在等待,则该函数不做任何操作。
notify_all()
唤醒所有等待在该条件变量上的线程。
六、条件变量的示例代码分析
6.1 生产者 - 消费者模型示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>std::mutex mtx;
std::condition_variable cv;
std::queue<int> buffer;
const unsigned int MAX_BUFFER_SIZE = 10;void producer(int id) { int data = 0; while (true) { // 模拟生产数据的时间std::this_thread::sleep_for(std::chrono::milliseconds(100));std::unique_lock<std::mutex> lock(mtx); // 等待缓冲区未满cv.wait(lock, [] { return buffer.size() < MAX_BUFFER_SIZE; }); // 生产数据并放入缓冲区buffer.push(data);std::cout << "生产者 " << id << " 生产了数据 " << data << std::endl;data++; // 通知消费者cv.notify_all(); }
}void consumer(int id) { while (true) {std::unique_lock<std::mutex> lock(mtx); // 等待缓冲区不为空cv.wait(lock, [] { return !buffer.empty(); }); // 从缓冲区取出数据 int data = buffer.front();buffer.pop();std::cout << "消费者 " << id << " 消费了数据 " << data << std::endl; // 通知生产者cv.notify_all(); // 模拟处理数据的时间lock.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(150)); }
}int main() {std::thread producers[2], consumers[2]; // 启动生产者线程 for (int i = 0; i < 2; ++i) {producers[i] = std::thread(producer, i); } // 启动消费者线程 for (int i = 0; i < 2; ++i) {consumers[i] = std::thread(consumer, i); } // 等待线程完成(此示例中线程是无限循环,可根据需要修改) for (int i = 0; i < 2; ++i) {producers[i].join();consumers[i].join(); } return 0;
}
代码分析
- 全局变量:
mtx
:互斥锁,用于保护共享缓冲区的访问,防止数据竞争。cv
:条件变量,用于线程间的等待和通知。buffer
:共享缓冲区,存放生产者生成的数据,供消费者消费。MAX_BUFFER_SIZE
:限制缓冲区的最大容量,防止过度填充。
- 生产者函数:
- 使用
unique_lock
获取互斥锁,确保对缓冲区的独占访问。 - 使用
cv.wait
等待缓冲区有空间,当缓冲区已满时,线程会释放锁并进入等待状态。 - 生产数据并放入缓冲区,然后调用
cv.notify_all
通知可能等待的消费者线程。
- 使用
- 消费者函数:
- 使用
unique_lock
获取互斥锁,确保对缓冲区的独占访问。 - 使用
cv.wait
等待缓冲区有数据,当缓冲区为空时,线程会释放锁并进入等待状态。 - 从缓冲区取出数据,然后调用
cv.notify_all
通知可能等待的生产者线程。
- 使用
6.2 线程交替打印示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool oddTurn = true;void printOdd() {for (int i = 1; i <= 10; i += 2) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return oddTurn; });std::cout << i << std::endl;oddTurn = false;cv.notify_one();}
}void printEven() {for (int i = 2; i <= 10; i += 2) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return !oddTurn; });std::cout << i << std::endl;oddTurn = true;cv.notify_one();}
}int main() {std::thread t1(printOdd);std::thread t2(printEven);t1.join();t2.join();return 0;
}
代码分析
- 全局变量:
mtx
:互斥锁,用于保护共享变量oddTurn
的访问。cv
:条件变量,用于线程间的等待和通知。oddTurn
:布尔变量,用于控制线程的执行顺序。
printOdd
函数:- 使用
unique_lock
获取互斥锁,确保对共享变量oddTurn
的独占访问。 - 使用
cv.wait
等待oddTurn
为true
,当oddTurn
为false
时,线程会释放锁并进入等待状态。 - 打印奇数,然后将
oddTurn
设置为false
,调用cv.notify_one
通知等待的线程。
- 使用
printEven
函数:- 使用
unique_lock
获取互斥锁,确保对共享变量oddTurn
的独占访问。 - 使用
cv.wait
等待oddTurn
为false
,当oddTurn
为true
时,线程会释放锁并进入等待状态。 - 打印偶数,然后将
oddTurn
设置为true
,调用cv.notify_one
通知等待的线程。
- 使用
通过这两个示例,我们可以看到条件变量在不同场景下的应用,以及如何通过条件变量和互斥锁的配合实现线程间的同步和通信。
七、总结
C++ 11 中的条件变量是一种强大的线程同步机制,它与互斥锁配合使用,可以实现高效的线程同步和通信。通过条件变量,我们可以避免线程的忙等待,提高程序的性能。
在使用条件变量时,需要注意以下几点:
- 必须在持有互斥锁的情况下调用
wait
函数。 wait
函数可能会发生虚假唤醒,因此建议使用带谓词的版本。- 通知操作 (
notify_one
或notify_all
) 不需要持有锁,但在修改共享变量时应该尽量减少锁的持有时间。
通过合理使用条件变量,可以解决多线程编程中的许多同步问题,如生产者 - 消费者模型、读者 - 写者模型等。希望本文能够帮助你更好地理解和使用 C++ 11 中的条件变量。