前一篇文章讲到了线程同步问题 ,但是有一种情况需要考虑:线程B需要线程A结束之后才能继续进行,但是如何来得知线程A已经结束了呢?没有学过线程之前可以使用标识符flag
标识线程A是否结束,但是此时线程B需要一直去检查标识符flag
。虽然空循环不会对线程有很大的影响,但是还有会一直占用线程。有没有一种机制,在线程A完成以后自动通知线程B然后线程B才载入运行。今天我们就来讲一下条件变量的问题。
举一个简单的例子,你在从家到学校的路上需要坐公交车,但是又很困想睡觉,却又怕睡过了。传统方式就是每2分钟起来一次,看是不是到站了,但是这样你就会睡不好。另外一种方式就是使用地图的提醒功能,快到到站的时候提醒你起来,这时你不用占用休息的时间。其实条件变量的作用就是闹钟 的作用而已。
bool flag = true;
void fun_1(){
while (flag){
cout << "wait!!" << endl;
this_thread::sleep_for(chrono::seconds(1));
}
cout << "线程B执行开始" << endl;
cout << "线程B执行结束" << endl;
}
void fun_2(){
cout << "线程A执行开始" << endl;
this_thread::sleep_for(chrono::seconds (5));
cout << "线程A执行结束" << endl;
flag = false;
}
当线程A执行结束之后修改标识符,然后线程B的循环结束,开始执行。执行结果如下:
在这里插入代码片
此时线程B一直占用着处理器资源,还有一种方式就是使用前面的同步的方法使用互斥元进行。源代码如下:
mutex mut;
void fun_1(){
lock_guard<mutex> lock(mut);
cout << "线程B执行开始" << endl;
cout << "线程B执行结束" << endl;
}
void fun_2(){
cout << "线程A执行开始" << endl;
this_thread::sleep_for(chrono::seconds (5));
cout << "线程A执行结束" << endl;
mut.unlock();
}
int main(){
thread t_1(fun_1);
thread t_2(fun_2);
mut.lock();
t_1.join();
t_2.join();
}
在主线程中先把互斥元锁住,然后在线程A执行完毕之后再将互斥元解锁,线程B继续执行。虽然这样的方式可以实现异步,但是需要在主线程中锁定互斥元。这样很容易出现以往导致线程死锁。
下面就来看看今天引入的新方法,条件变量。
关键词为condition_variable
和condition_variable_any
,都包含在
头文件中。前者仅限于和mutex
一起合作,后者则可以和所有能够实现互斥的东西合作。
condition_variable
有两类函数一些是用于等待的,一类是用于唤醒的。
用于等待的函数通常有三个:
wait()
:通常是两个参数,一个是互斥元,另一个是条件函数;当条件表达式为假时,释放互斥元进入阻塞状态,等待唤醒函数的唤醒。wait_for()
:通常是两个参数,一个是互斥元,另一个是等待时间;直接阻塞直到被唤醒或被超时。wait_until
:通常是两个参数,一个是互斥元,另一个是等待时刻;直接阻塞直到被唤醒或被超时。用于唤醒的函数通常也是两个:
notify_one()
:唤醒其中一个线程。notify_all()
:唤醒所有线程。下面就来用一用这些函数吧,
mutex mut;
condition_variable condition;
void fun_1(){
unique_lock<mutex> lock(mut);
condition.wait(lock);
cout << "线程B执行开始" << endl;
cout << "线程B执行结束" << endl;
}
void fun_2(){
cout << "线程A执行开始" << endl;
this_thread::sleep_for(chrono::seconds (5));
cout << "线程A执行结束" << endl;
condition.notify_one();
}
执行结果如下:
当线程B执行到wait()
函数时,释放lock
的锁定,然后线程B进入阻塞状态,等待被唤醒。线程A执行到notify_one()
时,唤醒一个线程,线程B被唤醒继续执行。
如果需要唤醒的线程多于一个的时候,会出现什么情况呢?
thread t_1(fun_1);
thread t_2(fun_2);
thread t_3(fun_3);
t_1.join();
t_2.join();
t_3.join();
此时只会唤醒一个线程,然后一直等待,并且唤醒的线程是随机的。
如果把notify_one
换成notify_all()
那么这个执行的结果就会不一致了。看一下下面这个例子。
mutex mut;
condition_variable condition;
void fun_1(){
unique_lock<mutex> lock(mut);
condition.wait(lock);
cout << "线程B执行开始 " << this_thread::get_id() << endl;
cout << "线程B执行结束 " << this_thread::get_id() << endl;
}
void fun_2(){
cout << "线程A执行开始" << endl;
this_thread::sleep_for(chrono::seconds (1));
cout << "线程A执行结束" << endl;
condition.notify_all();
}
void fun_3(){
unique_lock<mutex> lock(mut);
condition.wait(lock);
cout << "线程C执行开始 " << this_thread::get_id() << endl;
cout << "线程C执行结束 " << this_thread::get_id() << endl;
}
wait_until()
使用的频率不高,所以我们这里只介绍wait_for()
。至少需要传入两个参数,一个锁一个时间。看看下面这个例子。
mutex mut;
condition_variable condition;
void fun_1(){
unique_lock<mutex> lock(mut);
condition.wait(lock);
cout << "线程B执行开始 " << this_thread::get_id() << endl;
cout << "线程B执行结束 " << this_thread::get_id() << endl;
}
void fun_2(){
cout << "线程A执行开始" << endl;
this_thread::sleep_for(chrono::seconds (4));
cout << "线程A执行结束" << endl;
condition.notify_all();
}
void fun_3(){
unique_lock<mutex> lock(mut);
condition.wait_for(lock,chrono::seconds(2));
cout << "线程C执行开始 " << this_thread::get_id() << endl;
cout << "线程C执行结束 " << this_thread::get_id() << endl;
}
执行结果如下:
线程A先开始,需要执行4后才能结束,然后唤醒其他的线程。线程B则需要被条件变量唤醒才能继续运行。线程C则仅等待2秒,2秒后自动结束等待继续执行。这个结果也是如代码那样,线程C先于线程A结束前开始运行。
这里面用了一个chrono
类,这是C++11引入的全新的时间类,有兴趣可以去看一下,比之前的ctime
好用很多,而且易于理解。
虚假唤醒的缘故导致if判断在多线程环境下出错,因为它不再判断条件是否满足,继续从wait()方法之后执行。而while还会再次判断条件是否满足,如果不满足就不会执行。
void fun_3(){
unique_lock<mutex> lock(mut);
if (condition.wait_for(lock,chrono::seconds(2),[]{return false;}));
cout << "线程C执行开始 " << this_thread::get_id() << endl;
cout << "线程C执行结束 " << this_thread::get_id() << endl;
}
正常来说这个fun_3
所在的线程永远不能被唤醒,但是由于这个if的缘故,在测试中会发现确实被唤醒了。这就是伪唤醒。
因为if仅仅只能在第一次进入时判断,而被唤醒时不再进行判断,所以此时在条件不成立的时候也会出现被唤醒的情况。可以使用while替代if来避免这个问题。
void fun_3(){
unique_lock<mutex> lock(mut);
while (condition.wait_for(lock,chrono::seconds(2),[]{return true;}));
cout << "线程C执行开始 " << this_thread::get_id() << endl;
cout << "线程C执行结束 " << this_thread::get_id() << endl;
}
此时就线程C就会一直阻塞,
但是为什么线程B也一直被阻塞呢? 这是因为线程C一直在尝试while循环尝试解锁,所以一直占用则mut互斥元导致线程B无法获得互斥元结束等待。所以在这个里面我们需要注意这个问题,但是while也不是一个很好的方法,因为还是回到了最开始的那个问题,while会占用空间,占用CPU资源。
条件变量的使用使得线程之间异步通信有了一个基础,可以各自线程单独运行,最终合成结果。这样就可以最大化利用CPU资源,但是这样也会出现很多安全方面的问题。这就需要大量的实践经验来积累。下一部分将去看看,如何将条件变量包装得更加好看,更加好用!