
在现代计算机编程的广阔天地里,多核处理器的普及宛如一阵春风,吹开了多线程编程的繁花。多线程编程,这一构建高性能应用程序的利器,逐渐成为了开发者们手中的法宝。然而,在多线程程序的世界里,线程间的同步和通信就像是一座难以跨越的大山,横亘在开发者面前。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 中的条件变量。
八、条件变量的示意图

















