文章目录
C++11多线程(四) 条件变量
condition_variable
condition_variable是一个类,由字面意思可以很清楚地看出它与条件相关。也就是条件变量。
condition_variable没有特别的构造方法,所以我们直接声明就好了。
它的拷贝构造函数和等号运算符重载函数也被禁止了。
condition_variable s;
条件变量也就是我们所说的通过判断(bool)来对锁进行控制,不太理解没有关系,我们接着往下看。
condition_variable::wait()
首先来看一下condition_variable::wait()的函数源码
void wait(unique_lock<mutex>& _Lck) //第一种
{ // wait for signal
_Cnd_waitX(_Mycnd(), _Lck.mutex()->_Mymtx());
}
template<class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)//第二种
{ // wait for signal and test predicate
while (!_Pred())
wait(_Lck);
}
从里面我们可以清楚的看到,condition_variable是和unique_lock配合使用的。
原因如下
-
unique_lock具有lock(),unlock()函数。
-
unique_lock重载了带右值引用参数的拷贝构造函数和赋值运算符重载函数,这样它就可以应用在函数传参或者参数返回中使用。
记不得的小伙伴回顾上一节
那wait()的作用是什么呢?
- 将当前线程的状态设置成睡眠状态,直至被唤醒。
- 调用传入的mutex的unlock() , 释放出锁。
好了,源码看不懂的话我们就用更简单的语言讲解。
第一个构造函数要求我们传入一个unique_lock
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class Test
{
public:
Test() = default;
~Test() = default;
void f1();
void f2();
private:
mutex mtx;
condition_variable con_var; //定义条件变量
};
void Test::f1() {
unique_lock<mutex> uni_lock(mtx); //创建unique_lock
con_var.wait(uni_lock); //调用condition_variable::lock()
cout << "f1()" << endl;
}
void Test::f2() {
lock_guard<mutex> _guard_lock(mtx);
con_var.notify_one(); //在f2中唤醒f1中的线程,并让它获得锁继续执行先前的代码。稍后讲解
cout << "f2()" << endl;
}
int main() {
Test T;
while (1) {
thread t1(&Test::f1,&T);
thread t2(&Test::f2,&T);
thread t3(&Test::f2, &T);
thread t4(&Test::f2, &T);
t1.join();
t2.join();
t3.join();
t4.join();
}
return 0;
}
上面的代码用的是第一种wait方式,下面我们来解释一下第二种。
第二种其实也很简单,第一个参数照旧传入unique_lock,第二个参数我们传入一个bool值,来判断是否需要转换到睡眠状态。
con_var.wait(uni_lock,true/false);//这里的/表示任选其一
//但实际情况下我们一般使用匿名函数
con_var.wait(uni_lock,[]()->bool{
return _vec.empty();//_vec是一个vector<int>,这里只是展示方法。
//类似的可以回顾之前封装queue的代码,当m_queue为空时线程变为睡眠状态
//也可以自己思考用在什么地方,有助于加深记忆
});
在实际的代码过程中,还是建议我们多多实用第二种方法,由于匿名函数的加入,使得调整线程状态变得更加灵活。
自己可以尝试打一下代码试试,这样记忆也会更深刻。
wait_until,wait_for
还记得我们之前提到的try_lock_until和try_lock_for吗?//第二节里
等待到一个时间点和等待多久。
不过try_lock_until,try_lock_for是尝试获得锁,wait_until,wait_for则是进入休眠。
condition_variable也是有两个类似方法的,
但由于其重载形式较多,这里给出最常用的两个形式。
//wait_for 阻塞线程直到被唤醒或者阻塞时长超过指定时长
if(con_var.wait_for(uni_lock,chrono::milliseconds(1000))==cv_status::timeout) {//chrono之前有提到过,C++11下的时间库
cout<<"Helloworld"<<endl;
}
//wait_until 阻塞线程直到被唤醒或者阻塞时间超过指定时间点
if(cv.wait_until(con_var, chrono::steady_clock::now() + std::chrono::milliseconds timeout(100)){
cout<<"Helloworld"
}
notify_one,notify_all
还记得上面提到的那段代码吗?
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class Test
{
public:
Test() = default;
~Test() = default;
void f1();
void f2();
private:
mutex mtx;
condition_variable con_var; //定义条件变量
};
void Test::f1() {
unique_lock<mutex> uni_lock(mtx,); //创建unique_lock
con_var.wait(uni_lock);//调用condition_variable::lock()
cout << "f1()" << endl;
}
void Test::f2() {
lock_guard<mutex> _guard_lock(mtx);
con_var.notify_one();
cout << "f2()" << endl;
}
int main() {
Test T;
thread t1(&Test::f1, &T);
thread t2(&Test::f2, &T);
thread t3(&Test::f2, &T);
thread t4(&Test::f2, &T);
t1.join();
t2.join();
return 0;
}
有休眠就有唤醒。当线程条件就绪时,我们就可以将其唤醒,这里使用的是notify_one.
notify_one,notify_all两个都可以用作唤醒,他们也都是condition_variable的一个成员函数。
从名称也可以看出他们的作用
-
notify_one是用来唤醒一个线程的。
-
notify_all是用来唤醒所有休眠线程的。
值得注意的是notify_one是唤醒睡眠状态里的随机一个线程,并不能指定唤醒某个睡眠线程。
想想刚刚的模拟读取m_que代码
con_var.wait(uni_lock,true/false);//这里的/表示任选其一 //但实际情况下我们一般使用匿名函数 con_var.wait(uni_lock,[]()->bool{//[]()这里是匿名函数的使用,不懂的查阅一下相关资料,很实用的特性 return m_que.empty();//m_que是一个queue<int>,这里只是展示方法。 //可以回顾之前封装queue的代码,去自己实现一下当m_queue为空时,线程变为睡眠状态,防止Pop()抛出异常 //也可以自己思考用在什么地方,有助于加深记忆 }); m_que.Pop(); //另一个函数中我们就可以去唤醒 m_que.push(1); //当我们传入数字后,m_que就不再为空了。 con_var.notify_one();//我们就可以唤醒随机一个线程去读取 //当然我们也可以使用notify_all()去唤醒所有读取线程,但要注意异常
condition_variable_any
既然提到了condition_variable,就顺带一提condition_variable_any。
condition_variable只能接受unique_lock锁,但condition_variable_any却能接受任意形式的锁。
其余基本是一样的,因为本人用的不是很多就只能引个头,感兴趣的同学自行查询相关资料。
condition_variable的用法很简单多练习很容易上手,但C++所定义的condition远不如此,我也只能介绍一些我常用的。
好了,condition_variable的介绍就到这里,谢谢您的观看。