C++并发与多线程(5)——call_once和条件变量condition_varible
一、call_once
在多线程的环境下,有些时候我们不需要某给函数被调用多次或者某些变量被初始化多次,它们仅仅只需要被调用一次或者初始化一次即可。
为了解决上述多线程中出现的资源竞争导致的数据不一致问题,我们大多数的处理方法就是使用互斥锁来处理。在C++11中提供了最新的处理方法:使用std::call_once()
函数模板来处理,需包含头文件#include<mutex>
。
头文件内对此的定义如下:
template <class Fn, class... Args>
void call_once (once_flag& flag, Fn&& fn, Args&&... args);
第一个参数是一个标记,第二个参数可以是函数、成员函数、函数对象、lambda函数。
call_once()
需要与一个标记结合使用,这个标记为std::once_flag
。其实once_flag
是一个结构,call_once()
就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。
多个线程同时执行时,一个线程会等待另一个线程先执行。
一个程序的例子如下:
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag g_flag;
void func()
{
std::cout << "func() " << std::this_thread::get_id << std::endl;
}
void do_once()
{
std::call_once(g_flag, func);
}
int main()
{
std::thread mythread1(do_once);
std::thread mythread2(do_once);
std::thread mythread3(do_once);
mythread1.join();
mythread2.join();
mythread3.join();
return 0;
}
该程序的输出结果只有一行func()和线程id。
call_once
函数常用于单例设计模式。整个项目中,有某个或者某些特殊的类,只能创建一个属于该类的对象,该类被称为单例类,这样的设计模式叫单例模式。如果需要在自己创建的线程中来创建单例类的对象,这种线程可能不止一个。
二、条件变量std::condition_varible
上一篇文章的程序中,消息出队函数的循环内部,在每次判断是否为空之前都要进行锁定,影响执行效率。有一种双重锁定的办法,在锁定之前再判断一次队列是否为空,程序修改如下:
void outMsgRecvQueue(void)
{
int command = 0;
for (int i = 0; i < 10000; ++i)
{
if (!msgRecvQueue.empty())//双重锁定,再判断一次
{
unique_lock<mutex> myUnique(myMutex);
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); //代表执行了命令
msgRecvQueue.pop_front();
cout << "outMsgRecvQueue()执行,移除一个元素" << i << endl;
}
else {
cout << "outMsgRecvQueue()执行,消息队列为空" << i << endl;
}
}
}
return;
}
这种办法经常被许多人用,是一种双重检查。但是每一次循环总是要判断(轮询)很费时间,为了简化这种写法,且加快程序执行效率,C++引入了条件变量的类:std::condition_varible
。cppreference对此的描述是:
Condition variable
A condition variable is an object able to block the calling thread until notified to resume.It uses a unique_lock (over a mutex) to lock the thread when one of its wait functions is called. The thread remains blocked until woken up by another thread that calls a notification function on the same condition_variable object.
Objects of type condition_variable always use unique_lock to wait: for an alternative that works with any kind of lockable type, see condition_variable_any
机翻:
条件变量是一个能够阻止调用线程的对象,直到通知继续。
当调用线程的一个等待函数时,它使用一个unique_lock(在互斥锁上)来锁定线程。该线程保持阻塞状态,直到被另一个线程唤醒,该线程调用同一条件变量对象上的通知函数。
condition_variable类型的对象总是使用unique_ lock等待:有关可与任何类型的可锁定类型一起使用的替代方法,请参阅condition_variable_any
可以这样定义:std::condition_varible myCond
。
condition_varible
是一个和条件相关的类,就是等待一个条件达成。它有两个搭配使用的成员函数:wait()
和notify_one()
。
1.wait()
作用:起到一个阻塞的作用,但在需要的时候会解除阻塞。
格式:第一个参数是unique_lock类对象(lock_guard是不行的),第二个参数可以是函数名或lambda表达式。函数返回值必须是一个布尔值。 第二个参数可以是缺省值。
-
如果第二个参数的函数返回值是false,那么
wait()
将解锁互斥量,并阻塞到本行(该线程不往下执行),阻塞到其他某个线程调用notify_one()
成员函数为止。 -
如果第二个参数的函数返回值是true,那么
wait()
将直接返回并继续执行下面的代码。 -
如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样。
例如下面程序:
unique_lock<mutex> myUnique(myMutex);
myCond.wait(myUnique, [this] {
if (!msgRecvQueue.empty())
return true;
return false;
});
//代码段...
如果lambda表达式返回true,执行下面代码;如果返回false,则解开互斥锁,自己停止执行,让其他线程去拿锁并执行。
2.notify_one()
作用:通知一个线程的wait()
,唤醒线程去尝试拿锁。
还有类似的函数:notify_all()
,作用是通知所有的线程。
我们用下面程序来说明工作原理:
//【线程A】
unique_lock<mutex> myUnique(myMutex);
myCond.wait(myUnique, [this] {
if (!msgRecvQueue.empty())
return true;
return false;
});
//代码段...
//【线程B】
//代码段...
myCond.notify_one();
非常关键的内容!必看!
如果线程B没有调用notify_one()
,线程A会一直卡在wait()
这里不走。【线程A处于睡眠状态】
当线程B用notify_one()
将线程A的wait()
唤醒后,线程A将进行如下步骤:
(1)wait()
不断尝试获取互斥量锁,如果获取不到锁,那么线程A就卡在wait()
这里等待获取 【线程A处于唤醒状态且没有获得锁】; 如果获取到了,那么wait()就继续执行,获取到了锁。【线程A处于唤醒状态且获得了锁】
(2)开始判断第二个参数,即判断函数或lambda表达式的返回值:
(2.1)如果返回值为false,那wait()
对互斥量解锁,然后又休眠,等待再次被notify_one()
唤醒。【线程A处于唤醒状态,获得了锁后发现还没到时候,解锁后又睡回去了】
(2.2)如果返回值为true,则wait()
返回,流程可以继续执行(此时互斥量已被锁住)。【线程A处于唤醒状态,获得了锁后发现已经到时候,开始工作!(不解锁)】 因此,流程只要走到了wait()
下面则互斥量一定被锁住了。
3.完整例程和一些思考
这是把上面的完整程序改了一下:
#include <iostream>
#include <list>
#include <thread>
#include <mutex>
using namespace std;
class A {
public:
void outMsgRecvQueue(void)
{
int command = 0;
while(true)
{
unique_lock<mutex> myUnique(myMutex);
myCond.wait(myUnique, [this] {
if (!msgRecvQueue.empty())
return true;
return false;
});
//等待被其他进程唤醒,唤醒后还要判断能否拿到锁,
//以及是否需要拿到锁,如果没拿到或者没必要拿就继续睡了
command = msgRecvQueue.front(); //代表执行了命令
msgRecvQueue.pop_front();
cout << "outMsgRecvQueue()执行,移除一个元素" << command << endl;
myUnique.unlock();
}
return;
}
void inMsgRecvQueue(void)
{
for (int i = 0; i < 10000; ++i)
{
unique_lock<mutex> myUnique(myMutex);
//注意,这两个线程用的是同一把锁才行!
msgRecvQueue.push_back(i);
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
myCond.notify_one(); //尝试唤醒outMsgRecvQueue()里的wait()
}
return;
}
private:
list<int> msgRecvQueue;
mutex myMutex;
std::condition_variable myCond;
};
int main()
{
A myObja;
thread myOutMsgObj(&A::outMsgRecvQueue, &myObja);
thread myInMsgObj(&A::inMsgRecvQueue, &myObja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
对于上面的程序,老师上课时说的一些思考:
在大多数情况下,outMsgRecvQueue()
与inMsgRecvQueue()
并不是一对一执行的,所以当程序循环执行很多次以后,可能在队列中已经有了很多消息,但是,出队进程还是被唤醒一次只处理一条数据。这时可以考虑把出队进程多执行几次,或者对进队进程进行限流。