(条件变量)这一工具,如同一把利剑,为开发者们劈开了这座大山,提供了一种高效、灵活的线程同步机制。
条件变量 (Condition Variable) 是一种线程同步机制,它就像是一位公正的裁判,用于在线程之间等待某个条件的成立,并通知其他线程这一情况。它通常与互斥锁 (std::mutex
) 配合使用,就像是一对默契的搭档,以保证线程间的同步和数据访问的安全性。
简单来说,条件变量可以让一个线程阻塞等待特定条件的成立,当条件满足时,其他线程可以通过通知 (notify_one
或 notify_all
) 唤醒等待的线程。在 C++ 标准库中,条件变量通过以下两个类实现:
std::condition_variable
:适用于普通线程同步,就像是一把精准的手术刀,专门用于特定场景。std::condition_variable_any
:通用版本,支持与任意的锁类型配合使用,如同一个万能的工具箱,适用于各种复杂情况。条件变量和互斥锁的配合使用是线程同步的关键。互斥锁用于保护共享资源,防止多个线程同时访问导致数据不一致。而条件变量则用于在线程之间传递信号,通知线程何时可以继续执行。
当一个线程需要等待某个条件满足时,它会先获取互斥锁,然后检查条件是否满足。如果条件不满足,线程会调用条件变量的 wait
函数,释放互斥锁并进入等待状态。当其他线程修改了共享变量并满足条件时,它会调用条件变量的 notify_one
或 notify_all
函数,唤醒等待的线程。被唤醒的线程会重新获取互斥锁,继续执行。
这种配合方式可以避免线程的忙等待,提高程序的效率。例如,在生产者 - 消费者模型中,生产者线程会等待缓冲区中有足够空间后再生产新数据,消费者线程会等待缓冲区非空,然后从中取出数据。通过条件变量和互斥锁的配合,可以实现线程间的高效同步。
条件变量提供了以下几个核心操作:
wait
:阻塞当前线程,直到条件满足或被其他线程通知。就像是一个沉睡的巨人,等待着被唤醒的那一刻。notify_one
:通知一个等待中的线程。如同在黑暗中点亮一盏明灯,唤醒一个沉睡的灵魂。notify_all
:通知所有等待中的线程。仿佛是一声嘹亮的号角,唤醒所有沉睡的勇士。条件变量的一个经典应用场景是生产者 - 消费者模型。以下是一个简单的例子:
#include
#include
#include
#include
#include
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
函数,通知所有等待的线程条件已经满足。通过这个例子,我们可以看到条件变量在生产者 - 消费者模型中的重要作用。它可以实现线程间的高效同步,避免线程的忙等待,提高程序的性能。
条件变量的底层实现通常依赖于操作系统提供的同步原语,例如 POSIX 下的 pthread_cond_t
或 Windows API 的 CONDITION_VARIABLE
。C++ 标准库中的条件变量通过互斥锁 (std::mutex
) 和条件变量本身相互结合,形成了一种高效的线程同步机制。
核心机制包括:
wait
时,会先解锁关联的互斥锁并进入休眠状态。这样可以防止因线程阻塞导致的死锁。就像是一个聪明的旅行者,在等待的过程中不会占用太多资源。notify_one
或 notify_all
时,会唤醒一个或多个等待中的线程,并重新尝试获取互斥锁。如同一位使者,传递重要的信息,唤醒沉睡的人们。在没有条件变量的情况下,线程通常会采用“忙等待”的方式检查条件是否成立。这种方法会导致 CPU 资源的浪费。例如:
while (!condition) {
// 忙等待,浪费 CPU 资源
}
相比之下,条件变量的优点在于:
条件变量的 wait
函数可能会因虚假唤醒 (spurious wakeup) 而提前返回。因此,建议将 wait
的调用写成以下形式:
cv.wait(lock, [] { return condition; });
通过传入一个谓词函数,可以确保线程只有在条件成立时才会继续执行。这就像是一个严格的门卫,只有满足条件的人才能进入。
条件变量的通知操作 (notify_one
或 notify_all
) 不需要持有锁。因此,在修改共享变量并通知等待的线程时,应该尽量减少锁的持有时间,避免其他线程长时间等待。例如:
{
std::lock_guard<std::mutex> lock(mtx);
// 修改共享变量
}
cv.notify_one();
这样可以提高程序的并发性能,让更多的线程能够同时执行。
生产者 - 消费者模型是条件变量的经典应用场景。在这个模型中,生产者线程负责生产数据,消费者线程负责消费数据。通过条件变量和互斥锁的配合,可以实现线程间的高效同步,避免数据竞争和死锁。
读者 - 写者模型是另一个常见的应用场景。在这个模型中,读者线程可以同时读取共享资源,而写者线程需要独占访问共享资源。通过条件变量和互斥锁的配合,可以实现读者和写者之间的同步,避免数据不一致。
线程池是一种常见的并发编程模型,用于管理和复用线程。在线程池中,线程会等待任务的到来,当有任务时,线程会被唤醒并执行任务。通过条件变量和互斥锁的配合,可以实现线程池的高效管理,提高程序的性能。
std::condition_variable
std::condition_variable
是一个类,用于实现条件变量的基本功能。它只能与 std::unique_lock
一起使用。
std::condition_variable_any
std::condition_variable_any
是一个更通用的条件变量类,它可以与任何满足可锁定 (Lockable) 要求的锁类型一起使用,而不仅仅局限于 std::unique_lock
。
wait()
使当前线程阻塞,直到收到通知或发生虚假唤醒。调用该函数时,线程会释放其所持有的锁,进入等待状态。当收到通知后,线程会重新获取锁并继续执行。
有两个重载版本如下:
void wait( std::unique_lock& lock );
:无条件等待。template< class Predicate > void wait( std::unique_lock& lock, Predicate pred );
:等待直到 pred()
返回 true
,可以避免虚假唤醒。wait_for()
使当前线程阻塞,直到收到通知、发生虚假唤醒或达到指定的超时时间。返回值表示线程被唤醒的原因。
函数原型:template< class Rep, class Period > std::cv_status wait_for( std::unique_lock
wait_until()
使当前线程阻塞,直到收到通知、发生虚假唤醒或到达指定的时间点。返回值表示线程被唤醒的原因。
函数原型:template< class Clock, class Duration > std::cv_status wait_until( std::unique_lock
notify_one()
唤醒一个等待在该条件变量上的线程。如果没有线程在等待,则该函数不做任何操作。
notify_all()
唤醒所有等待在该条件变量上的线程。
#include
#include
#include
#include
#include
#include
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
通知可能等待的生产者线程。#include
#include
#include
#include
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 中的条件变量。