顾名思义,条件变量就是一种让线程满足的条件。需要和互斥锁配合使用
官方文档:http://www.cplusplus.com/reference/condition_variable/condition_variable/
需求:有时候,我们需要多个线程能够同时就绪,再继续往下执行,或者多个线程需要使用有限的互斥量,然后再继续执行。使线程之间从某处开始同步执行。
std::condition_variable是一个类,所以我们需要有个它的对象来使用。
成员函数wait()和notify_one()
wait和notify_one有以下规则。
wait用来等待一个条件,调用方式为函数
wait(unique_lock, bool)
其中第二个参数可以可调用对象或lambda表达式来返回。
如果第二个参数返回值为true,wait直接返回,继续执行。
如果第二个参数的返回值是false,那么wait()将解锁互斥量,并阻塞到本行
阻塞直到其他某个线程调用notif_one时为止
如果wait()没有使用第二个参数,那么相当于默认使用false
当其他线程用notify_one将本wait唤醒后,该线程从此处继续执行
wait不断尝试重新获取互斥锁,如果获取不到,那么继续堵塞,但此时的阻塞等同于lock();
如果wait有第二个参数(lambda表达式),就判断这个值,如果false,则同上,解锁lock(),进入阻塞,等待notify调用
如果wait第二个参数返回为true,则成功继续执行(此时是锁着的)
如果wait没有第二个参数,wait返回,相当于true。
相当于在notify_one调用后,线程重新执行wait,并在这之前执行一个lock()。
参考流程如图所示:
注意解锁时机。
实例还是使用之前的消息队列的代码,但我们重写出队和入队的函数。
#include <iostream>
#include <mutex>
#include <condition_variable>
using namespace std;
class MsgQueue
{
public:
void InMsgQueue()
{
for (int i = 0; i < 10000; ++i)
{
unique_lock<mutex> my_unique_lock1(my_mutex);
msgQueue.push_back(i);
my_condition.notify_one(); //唤醒wait的线程
//...其他代码
}
return;
}
void OutMsgQueue()
{
int command = 0;
while (true)//循环执行,等待条件
{
unique_lock<mutex> my_unique_lock2(my_mutex);
my_condition.wait(my_unique_lock2, [this] //lambda表达式就是一个可调用对象(函数)
{
if (!msgQueue.empty())
return true;
return false;
});
command = msgQueue.front();
msgQueue.pop_front();
my_unique_lock2.unlock();//及时解锁
cout << "取出元素:" << command << endl;
}
}
private:
list<int> msgQueue;//使用链表,头尾删除更高效
mutex my_mutex;
condition_variable my_condition; //条件变量
};
int main()
{
MsgQueue queue;
thread InQueueObj(&MsgQueue::InMsgQueue, &queue);
thread OutQueueObj(&MsgQueue::OutMsgQueue, &queue);
InQueueObj.join();
OutQueueObj.join();
cout << "主线程结束" << endl;
return 0;
}
通过改造以后,比较之前我们的出队和入队函数(已优化):
bool isEnd = false;
void InMsgQueue()
{
for (int i = 0; i < 1000; i++)
{
unique_lock<mutex> my_guard(my_mutex);
msgQueue.push_back(i);
cout << "插入元素" << i << endl;
}
isEnd = true;
}
void OutMsgQueue()
{
for (int i = 0; i < 3000; i++)
{
if (!msgQueue.empty())
{
unique_lock<mutex> my_guard(my_mutex);
cout << "取出元素" << msgQueue.front() << endl;
msgQueue.pop_front();
}
else if (isEnd && msgQueue.empty())
{
return;
}
else
{
//消息队列为空
cout << "无请求" << endl;
}
}
}
之前的出队函数主要执行的步骤在if (!msgQueue.empty())这一步,那函数的时间复杂度就几乎为O(n),n在实际运行的时候相当于无穷(因为需要不断检测),导致大量的CPU浪费在判空上面,使用条件变量后,只有入队函数执行后才会去执行判空之类的操作,时间复杂度为O(入队请求数)节约了系统开销。
注意:这里很容易存在一种情况,就是在获取锁的问题上,由于wait的性质,它拥有锁的能力不一定比其他直接能够lock的线程强,比如上面的例子中,很容易出现入队线程连续拥有了锁,往其中扔了好几个数据,而wait的线程只有老老实实的等着,虽然整体影响不大,但出队的速度会明显慢于入队的速度,(或者入队的速度慢于出队的速度),这种现象是存在的,这时候就要考虑项目容忍的时间。
通过和第四节的代码耗时比较,第四节的要更快一点,而且使用条件变量还需要写更多的东西去模拟实例。通过观察还发现,连续插入1000条数据的测试中,wait的线程执行速度远跟不上插入的线程,某次的试验如图所示:
另外,notify不一定对线程的唤醒每次都能成功,有可能wait正在干活呢?
多线程的环境是复杂多变的,我们要充分考虑各种情况。如果读者尝试过运行以上代码,会发现一个严重的问题,那就是没有给出跳出wait的办法。上面的代码,虽然在结束前能稳定运行,但随着最后一个元素取出后,入队线程已经执行完毕,主线程仍在等待出队线程执行完毕,而出队线程会无限卡在wait那里。如果读者自己再仔细深究condition_variable的使用,会发现可能没有那么轻松,上面很简单的几个操作都有很多隐患,更不要说在大型项目里错综复杂的逻辑。
个人建议:在没有把握的时候,慎重使用condition_variable。
notify_all()
看名字就很明显了,当我们有多个线程需要被唤醒的时候,notify_all就登场了。
上面的代码,使用wait的函数有一个明显的问题就是抢夺处理机的能力不强,导致输出速度慢于输入速度,解决问题的方法多种多样,要么限流,要么增加输出的吞吐量,一次取多个数据,或者用更多的线程来取数据。
async future
async是个函数模板,用来启动一个异步任务,启动一个异步任务后,返回一个future对象。
future对象里面含有线程入口函数所返回的结果。总的来说就是使用async和future来快速启动一个线程完成函数,并在future中获得结果。示例:
int threadFunc()
{
cout << "thread start,thread id =" << this_thread::get_id() << endl;
chrono::milliseconds dura(5000);
this_thread::sleep_for(dura);
cout << "thread end,thread id = " << this_thread::get_id() << endl;
return 5;
}
int main()
{
future<int> res = async(threadFunc);
cout << res.get() << endl;
cout << "主线程结束" << endl;
return 0;
}
get函数会有阻塞的作用,只有线程函数有了返回值后,get才会继续运行。
我们通过额外向async传递一个参数,该参数类型是std::launch类型(枚举类型),来达到一些特殊的目的。
std::launch::deferred:表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行
std::launch::async,在调用async的时候就创建线程
思考:如果wait()或者get()没有被调用,那么线程会执行吗?经过测验,是不会的,甚至线程都没有被创建。async()函数本身就是默认使用的std::launch::async标记