条件变量:https://zh.cppreference.com/w/cpp/thread/condition_variable
condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。
条件变量一般与互斥锁一起使用。
三个线程轮流打印0~100.
目标:是三个线程分别依次打印1,2,3…100
#include <iostream>
#include <thread>
#include <mutex> // 互斥量
#include <condition_variable> // 条件变量
using namespace std;
/*
// 版本一:不加信号量,互斥量,也可以完成,但会造成“空耗cpu”
例如在fun0中,不满足number%3==0条件,
则一直进行循环,直至线程时间片到达,切换线程
*/
void fun0() // 0 3
{
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
}
//cout << ".";
}
}
void fun1() // 1 4
{
while (number <= 100)
{
while (number % 3 == 1 && number <= 100)
{
cout << "t1_" << number << " ";
number += 1;
}
//cout << ".";
}
}
void fun2() // 2 5
{
while (number <= 100)
{
while (number % 3 == 2 && number <= 100)
{
cout << "t2_" << number << " ";
number += 1;
}
//cout << ".";
}
}
int main()
{
thread t0(fun0);
thread t1(fun1);
thread t2(fun2);
t0.join();
t1.join();
t2.join();
return 0;
}
缺点:会使得CPU空耗大量时间。取消上边的cout << ".";
注释可以看到cpu空耗情况。
使用条件变量
条件变量:
notify_one()(随机唤醒一个等待的线程)
notify_all()(唤醒所有等待的线程)
wait()是条件变量的成员函数,堵塞,等待唤醒
如果第二个参数lambda表达式返回值是false,
那么wait将解锁第一个参数(互斥量),并堵塞到本行。
堵塞到其他某个线程调用notify_one()成员函数为止
notify_one();//尝试吧wait的线程唤醒,执行完这行,wait就被唤醒了
当其他notify_one()将wait唤醒之后,wait不断尝试获取互斥量锁,
如果获取不到,流程就卡在wait这里等着获取。
如果获取到了,wait就获取到锁(就等于上锁);
这里存在一个问题,在下面的程序片段中,我们有Aa方案和Bb方案使用条件变量。
void fun0() // 0 3
{
unique_lock < std::mutex> locker(g_mtx); // 唯一性锁
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
// cv.wait(locker); // 等待 A ,A-a 先等待后唤醒
// 阻塞,等待其他线程唤醒
cv.notify_one(); // 唤醒 B ,B-b 先唤醒后等待
// 通知别的线程,唤醒其他线程
}
cout << ".a";
// cv.notify_one(); // 唤醒 a
cv.wait(locker); // 等待 b
}
cv.notify_one(); // 唤醒最后一个阻塞线程
}
等待和唤醒调用的时机很重要,如果我们使用Aa方案,那么可能会出现两种情况。
- 第一个执行到此处的线程将会进入等待状态,而其他线程因为没要满足进入循环的条件将会空耗时间。
- 三个线程刚好顺序执行,则三个线程最后都陷入了等待状态,但是由于没有额外的线程进行唤醒,会使得程序陷入循环等待状态。
因此,我们需要使用Bb方案。
/*
// 版本二:加锁
*/
void fun0() // 0 3
{
unique_lock < std::mutex> locker(g_mtx); // 唯一性锁
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
cv.notify_one(); // 唤醒 B ,B-b 先唤醒后等待
// 通知别的线程,唤醒其他线程
}
cout << ".";
cv.wait(locker); // 等待 b
}
cv.notify_one(); // 唤醒最后一个阻塞线程
}
void fun1() // 1 4
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 1 && number <= 100)
{
cout << "t1_" << number << " ";
number += 1;
cv.notify_one();
}
cout << ".";
cv.wait(locker);
}
// 打印100,最后阻塞线程为fun1
cv.notify_one();
}
void fun2() // 2 5
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 2 && number <= 100)
{
cout << "t2_" << number << " ";
number += 1;
cv.notify_one();
}
cout << ".";
cv.wait(locker);
}
cv.notify_one();
}
int main()
{
thread t0(fun0);
thread t1(fun1);
thread t2(fun2);
t0.join();
t1.join();
t2.join();
return 0;
}
此方案依然存在问题。
- 问题1:唤醒的线程,如果不是我们需要的线程,就会空耗被唤醒线程直至再次唤醒其他线程,这个过程一直到我们需要的线程被唤醒。(有可能出现死锁情况)
// 在程序中,输出两个‘.’表示有空耗 - 解决方案:使用 notify_all 唤醒所有其他线程
- 问题2:最后一个线程使用wait后,阻塞,不会有其他线程将其唤醒
- 解决方案:在最后一个线程wait时,在其他的任意一个可以正常退出的线程中调用一次唤醒程序,比如打印100个数,fun1最后退出,可以在fun0,或fun2中调用一次唤醒函数
如下图所示出现死锁情况:
修改方案,使用notify_all唤醒
/*
// 版本三:加锁
使用notify_all
线程退出时,调用一次唤醒函数
*/
void fun0() // 0 3
{
unique_lock < std::mutex> locker(g_mtx); // 唯一性锁
while (number <= 100)
{
while (number % 3 == 0 && number <= 100)
{
cout << "t0_" << number << " ";
number += 1;
// cv.wait(locker); // 等待 A ,A-a 先等待后唤醒
// 阻塞,等待其他线程唤醒
cv.notify_all(); // 唤醒 B ,B-b 先唤醒后等待
// 通知别的线程,唤醒其他线程
}
cout << ".";
// cv.notify_one(); // 唤醒 a
cv.wait(locker); // 等待 b
}
cv.notify_all(); // 唤醒最后一个阻塞线程
}
void fun1() // 1 4
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 1 && number <= 100)
{
cout << "t1_" << number << " ";
number += 1;
cv.notify_all();
}
cout << ".";
cv.wait(locker);
}
// 打印100,最后阻塞线程为fun1
cv.notify_all();
}
void fun2() // 2 5
{
unique_lock < std::mutex> locker(g_mtx);
while (number <= 100)
{
while (number % 3 == 2 && number <= 100)
{
cout << "t2_" << number << " ";
number += 1;
cv.notify_all();
}
cout << ".";
cv.wait(locker);
}
cv.notify_all();
}
int main()
{
thread t0(fun0);
thread t1(fun1);
thread t2(fun2);
t0.join();
t1.join();
t2.join();
return 0;
}