std::condition_variable
是C++11引入的一个同步机制,主要用于在线程之间进行事件通知协调。它是标准库中的一个类,声明在 <condition_variable>
头文件中。std::condition_variable
允许一个或多个线程等待另一个线程发送的信号(通过条件变量来实现的),从而基于某些条件进行协调动作。
wait函数
//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);
函数说明:
- 调用第一个版本的wait函数时只需要传入一个互斥锁,线程调用wait后会立即被阻塞,直到被唤醒。
- 调用第二个版本的wait函数时除了需要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。
为什么调用wait系列函数时需要传入一个互斥锁?
- 因为wait系列函数一般是在临界区中调用的,为了让当前线程调用wait阻塞时其他线程能够获取到锁,因此调用wait系列函数时需要传入一个互斥锁,当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。
- 因此wait系列函数实际上有两个功能,一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。
为什么调用wait系列函数时,传入互斥锁的类型必须是unique_lock?
- 条件变量在等待时需要释放互斥锁,并在条件满足时重新获取锁。
std::unique_lock
提供了这种能力。它可以在内部处理锁的解锁和重新加锁操作,这是一个非常关键的功能。在调用条件变量的wait
方法时,std::unique_lock
会在等待前解锁互斥锁,并在等待结束(无论是条件满足还是虚假唤醒)后重新锁定互斥锁。 - 条件变量在等待时需要释放互斥锁,并在条件满足时重新获取锁。
std::unique_lock
提供了这种能力。它可以在内部处理锁的解锁和重新加锁操作,这是一个非常关键的功能。在调用条件变量的wait
方法时,std::unique_lock
会在等待前解锁互斥锁,并在等待结束(无论是条件满足还是虚假唤醒)后重新锁定互斥锁。
notify系列成员函数
notify系列成员函数的作用就是唤醒等待的线程,包括notify_one和notify_all。
- notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
- notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。
注意: 条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队。
虚假唤醒(Spurious Wakeup)
虚假唤醒(Spurious Wakeup) 是指线程没有明确的通知却从等待状态被唤醒的情况。这种现象虽然在理论上是可能的,但在某些平台上也会真实发生。因此,等待条件的线程必须在唤醒后重新检查相关条件,而不仅仅依赖于通知。
为了应对虚假唤醒问题,通常的做法是在等待通知时使用 while
循环来检查条件。
实现两个、三个线程交替打印
该题目主要考察的就是线程的同步和互斥。
- 互斥:两个线程都在向控制台打印数据,为了保证两个线程的打印数据不会相互影响,因此需要对线程的打印过程进行加锁保护。
- 同步:两个线程必须交替进行打印,因此需要用到条件变量让两个线程进行同步,当一个线程打印完再唤醒另一个线程进行打印。
但如果只有同步和互斥是无法满足题目要求的。
- 首先,我们无法保证哪一个线程会先进行打印,不能说先创建的线程就一定先打印,后创建的线程先打印也是有可能的。
- 此外,有可能会出现某个线程连续多次打印的情况,比如线程1先创建并打印了一个数字,当线程1准备打印第二个数字的时候线程2可能还没有创建出来,或是线程2还没有在互斥锁上进行等待,这时线程1就会再次获取到锁进行打印。
因此,这里还需要定义一个flag变量,该变量的初始值设置为true。
- 假设让线程1打印奇数,线程2打印偶数。那么就让线程1调用wait函数阻塞等待时,传入的可调用对象返回flag的值,而让线程2调用wait函数阻塞等待时,传入的可调用对象返回!flag的值。
- 由于flag的初始值是true,就算线程2先获取到互斥锁也不能进行打印,因为最开始线程2调用wait函数时,会因为可调用对象的返回值为false而被阻塞,这就保证了线程1一定先进行打印。
- 为了让两个线程交替进行打印,因此两个线程每次打印后都需要更改flag的值,线程1打印完后将flag的值改为false并唤醒线程2,这时线程2被唤醒时其可调用对象的返回值就变成了true,这时线程2就可以进行打印了。
- 当线程2打印完后再将flag的值改为true并唤醒线程1,这时线程1就又可以打印了,就算线程2想要连续打印也不行,因为如果线程1不打印,那么线程2的可调用对象的返回值就一直为false,对于线程1也是一样的道理。
两个线程交替打印数字
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool turnA = true; // 轮到A线程打印
void printA() {
for (int i = 1; i <= 10; i += 2) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [](){ return turnA; });
std::cout << "Thread A: " << i << std::endl;
turnA = false;
cv.notify_all();
}
}
void printB() {
for (int i = 2; i <= 10; i += 2) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [](){ return !turnA; });
std::cout << "Thread B: " << i << std::endl;
turnA = true;
cv.notify_all();
}
}
int main() {
std::thread t1(printA);
std::thread t2(printB);
t1.join();
t2.join();
return 0;
}
三个线程交替打印数字
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
int turn = 0; // 用于标识哪个线程应当打印
void printNum(int threadId) {
for (int i = threadId; i <= 30; i += 3) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [threadId]{ return turn == threadId; });
std::cout << "Thread " << threadId << ": " << i << std::endl;
turn = (turn + 1) % 3;
cv.notify_all();
}
}
int main() {
std::thread t1(printNum, 0);
std::thread t2(printNum, 1);
std::thread t3(printNum, 2);
t1.join();
t2.join();
t3.join();
return 0;
}