文章目录
单例设计模式std::call_oncecondition_variable虚假唤醒参考本文系列大部分来自c++11并发与多线程视频课程的学习笔记,系列文章有(不定期更新维护):
C++并发与多线程(一)线程传参C++并发与多线程(二) 创建多个线程、数据共享问题分析、案例代码C++并发与多线程(三)单例设计模式与共享数据分析、call_once、condition_variable使用C++并发与多线程(四)async、future、packaged_task、promise、shared_futureC++并发与多线程(五)互斥量,atomic、与线程池
单例设计模式
在整个项目中,有某个或者某些特殊的类,只能创建一个属于该类的对象。单例类:只能生成一个对象。整个项目中,有某个或者某些特殊的类,属于该类的对象,我只能创建1
个,多了创建不了。设计代码如下:
#include <iostream>#include <mutex>using namespacestd;mutex myMutex;//懒汉模式class Singelton{public:static Singelton* getInstance() {// 双重锁定 提高效率if (instance == NULL) {// instance == NULL不代表一定没被new过,可能另外一个线程new了。但是这里已经进入循环了。lock_guard<mutex> myLockGua(myMutex);if (instance == NULL) {instance = new Singelton();}}return instance;}private:Singelton() {} // 私有化构造函数static Singelton* instance; // 静态成员变量};Singelton* Singelton::instance = NULL; // 给静态成员变量赋初值。//饿汉模式class Singelton2 {public:static Singelton2* getInstance() {return instance;}private:Singelton2() {} // 私有化构造函数static Singelton2 * instance; // 静态成员变量};Singelton2* Singelton2::instance = new Singelton2; // new Singelton2()也可以int main(void){// 单例类只能通过调用调用接口getInstance()创建,无法通过实例化创建。Singelton* singer = Singelton::getInstance();Singelton* singer2 = Singelton::getInstance();if (singer == singer2)cout << "二者是同一个实例" << endl;elsecout << "二者不是同一个实例" << endl;cout << "----------以下 是 饿汉式------------" << endl;Singelton2* singer3 = Singelton2::getInstance();Singelton2* singer4 = Singelton2::getInstance();if (singer3 == singer4)cout << "二者是同一个实例" << endl;elsecout << "二者不是同一个实例" << endl;return 0;}
单例设计模式中,对象构造函数是私有成员方法,创建对象的时候只能通过调用接口getInstance()
创建,无法通过实例化创建,因为构造函数被私有化了。程序输出结果为:
如果觉得在单例模式new
了一个对象,而没有自己delete
掉,这样不合理。可以增加一个类中类CGarhuishou
,new
一个单例类时创建一个静态的CGarhuishou
对象,这样在程序结束时会调用CGarhuishou
的析构函数,释放掉new
出来的单例对象。
class Singelton{public:static Singelton * getInstance() {if (instance == NULL) {static CGarhuishou huishou;instance = new Singelton;}return instance;}class CGarhuishou {public:~CGarhuishou(){if (Singelton::instance){delete Singelton::instance;Singelton::instance = NULL;}}};private:Singelton() {}static Singelton *instance;};Singelton * Singelton::instance = NULL;
单例类的对象可能会被多个线程使用到,一般我们可以在主线程中把该创建的对象创建了,该加载的数据加载了去。但是这种方式面临问题是:需要在自己创建的线程中来创建单例类的对象,并且这种线程可能不止一个。我们可能面临GetInstance()
这种成员函数需要互斥的情况。想要解决这个问题的话,我们可以在加锁前判断m_instance
是否为空,否则每次调用Singelton::getInstance()
都要加锁,十分影响效率。
std::call_once
std::call_once()
是一个函数模板,也是C++11
新引入的函数。该函数的第一个参数为标记,第二个参数是一个函数名(比如我们有个参数a
函数,那么它的第二个参数就是a()
)。它的功能是:能够保证函数a()
只被调用一次。具备互斥量的能力,而且比互斥量消耗的资源更少,更高效。call_once()
需要与一个标记结合使用,这个标记为std::once_flag;
其实once_flag
是一个结构,call_once()
就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。
#include <iostream>#include <mutex>using namespace std;mutex myMutex;//懒汉模式once_flag g_flag;class Singelton{public:static Singelton* getInstance() {call_once(g_flag, CreateInstance); //两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕return instance;}static void CreateInstance(){instance = new Singelton();}private:Singelton() {} // 私有化构造函数static Singelton* instance; // 静态成员变量};Singelton* Singelton::instance = NULL; // 给静态成员变量赋初值。//饿汉模式class Singelton2 {public:static Singelton2* getInstance() {return instance;}private:Singelton2() {} // 私有化构造函数static Singelton2 * instance; // 静态成员变量};Singelton2* Singelton2::instance = new Singelton2; // new Singelton2()也可以int main(void){// 单例类只能通过调用调用接口getInstance()创建,无法通过实例化创建。Singelton* singer = Singelton::getInstance();Singelton* singer2 = Singelton::getInstance();if (singer == singer2)cout << "二者是同一个实例" << endl;elsecout << "二者不是同一个实例" << endl;cout << "---------- 以下 是 饿汉式 ------------" << endl;Singelton2* singer3 = Singelton2::getInstance();Singelton2* singer4 = Singelton2::getInstance();if (singer3 == singer4)cout << "二者是同一个实例" << endl;elsecout << "二者不是同一个实例" << endl;return 0;}
condition_variable
std::condition_variable
实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。比如说线程A
等待一个条件满足,线程B
完成了这个条件之后就通知线程A
让其继续往下执行。看如下代码:
#include <iostream>#include <thread>#include <list>#include <mutex>using namespace std;class A {public:void inMsgRecvQueue(){for (int i = 0; i < 100000; ++i){cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;{//lock_guard<mutex> sbguard(myMutex1);lock(myMutex1, myMutex2); // 只有等所有互斥量都锁住才能锁成功。//myMutex2.lock(); // 先锁2再锁1,就会产生死锁。//myMutex1.lock();msgRecvQueue.push_back(i);myMutex1.unlock(); // 解锁的时候先解锁哪一个就无所谓。myMutex2.unlock();}}}bool outMsgLULProc(){myMutex1.lock(); // 这里与之前的先锁2后锁1会产生死锁。myMutex2.lock();if (!msgRecvQueue.empty()){cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;msgRecvQueue.pop_front();myMutex2.unlock();myMutex1.unlock();return true;}myMutex2.unlock();myMutex1.unlock();return false;}void outMsgRecvQueue(){for (int i = 0; i < 100000; ++i){if (outMsgLULProc()){cout << "outMsgLULProc()执行了,取出一个元素。" << endl;}else{// 消息队列为空cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;}}}private:list<int> msgRecvQueue;mutex myMutex1;mutex myMutex2;};int main(){A myobja;mutex myMutex;thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);thread myInMsgObj(&A::inMsgRecvQueue, &myobja);myOutMsgObj.join();myInMsgObj.join();return 0;}
outMsgLULProc
函数在不停地加锁,判断是否为空,效率很低,我们可以通过双重锁定,避免先锁一下的低效行为:
bool outMsgLULProc(){if (!msgRecvQueue.empty()){myMutex1.lock(); // 这里与之前的先锁2后锁1会产生死锁。myMutex2.lock();if (!msgRecvQueue.empty()){cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;msgRecvQueue.pop_front();myMutex2.unlock();myMutex1.unlock();return true;}myMutex2.unlock();myMutex1.unlock();}return false;}
如果能把程序写成,当有数据的时候就来通知我们,我们再去取数据这种思路就很好。condition_variable
类就能帮助我们完成这样一件事情。
std::unique_lock<std::mutex> sbgurad(myMutex);// wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。// 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。cond.wait(sbgurad, [this]{// 一个lambda表达式,相当于一个可调用对象。if(!msgRecvQueue.empty()) return true;else return false;});
wait()
用来等一个东西。如果第二个参数的lambda
表达式返回值是false
,那么wait()
将解锁互斥量,并阻塞到本行。如果第二个参数的lambda
表达式返回值是true
,那么wait()
直接返回并继续执行。阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()
成员函数为止。如果没有第二个参数,那么效果跟第二个参数lambda
表达式返回false
效果一样。
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;std::unique_lock<std::mutex> sbgurad(myMutex);msgRecvQueue.push_back(i);cond.notify_one(); // 尝试把outMsgLULProc中的wait线程唤醒。
当其他线程用notify_one()
将本线程wait()
唤醒后,这个wait
恢复后:1、wait()
不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()
这里等待获取,如果获取到了,那么wait()
就继续执行,获取到了锁。如果wait
有第二个参数就判断这个lambda
表达式。a)
如果表达式为false
,那wait
又对互斥量解锁,然后又休眠,等待再次被notify_one()
唤醒。b)
如果lambda
表达式为true
,则wait
返回,流程可以继续执行(此时互斥量已被锁住)。
std::unique_lock<std::mutex> sbgurad(myMutex);// wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。// 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。cond.wait(sbgurad, [this]{// 一个lambda表达式,相当于一个可调用对象。if(!msgRecvQueue.empty()) return true;else return false;});
所有代码如下所示:
#include <iostream>#include <thread>#include <list>#include <mutex>using namespace std;class A {public:void inMsgRecvQueue(){for (int i = 0; i < 1000000000; ++i){cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;std::unique_lock<std::mutex> sbgurad(myMutex);msgRecvQueue.push_back(i);cond.notify_one(); // 尝试把outMsgRecvQueue中的wait线程唤醒。}}void outMsgRecvQueue(){while(true){std::unique_lock<std::mutex> sbgurad(myMutex);// wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。// 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。cond.wait(sbgurad, [this]{// 一个lambda表达式,相当于一个可调用对象。if(!msgRecvQueue.empty()) return true;else return false;});msgRecvQueue.pop_front();cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;sbgurad.unlock();}}private:list<int> msgRecvQueue;mutex myMutex;std::condition_variable cond;};int main(){A myobja;thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);thread myInMsgObj(&A::inMsgRecvQueue, &myobja);myOutMsgObj.join();myInMsgObj.join();return 0;}
上面的代码可能导致出现一种情况:因为outMsgRecvQueue()
与inMsgRecvQueue()
并不是一对一执行的,所以当程序循环执行很多次以后,可能在msgRecvQueue
中已经有了很多消息,但是,outMsgRecvQueue
还是被唤醒一次只处理一条数据。这时可以考虑outMsgRecvQueue
多执行几次,或者对inMsgRecvQueue
进行限流。
notify_one()
:通知一个线程的wait()
。notify_all()
:通知所有线程的wait()
。
虚假唤醒
notify_one
或者notify_all
唤醒wait()
后,实际有些线程可能不满足唤醒的条件,就会造成虚假唤醒,可以在wait
中再次进行判断解决虚假唤醒。如下代码中inMsgRecvQueue
收到数据之后,通过notify_one
通知其它线程,其它线程在wait()
函数处等待,条件满足之后往下执行。
#include <iostream>#include <thread>#include <list>#include <mutex>using namespace std;class A {public:void inMsgRecvQueue(){for (int i = 0; i < 1000000; ++i){cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;std::unique_lock<std::mutex> sbgurad(myMutex);msgRecvQueue.push_back(i);cond.notify_one(); // 尝试把outMsgRecvQueue中的wait线程唤醒。}}void outMsgRecvQueue(){while(true){std::unique_lock<std::mutex> sbgurad(myMutex);// wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。// 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。cond.wait(sbgurad, [this]{// 一个lambda表达式,相当于一个可调用对象。if(!msgRecvQueue.empty()) return true;else return false;});msgRecvQueue.pop_front();cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;sbgurad.unlock();}}private:list<int> msgRecvQueue;mutex myMutex;std::condition_variable cond;};int main(){A myobja;thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);thread myInMsgObj(&A::inMsgRecvQueue, &myobja);myOutMsgObj.join();myInMsgObj.join();return 0;}
如果往数据中插入一条数据,却多次调用notify_one()
的话(因为有时候我们需要确保数据中有元素时,wait
函数能够被唤醒),我们就可能存在虚假唤醒的情况。解决:wait
中要有第二个参数(lambda
),并且这个lambda
中要正确判断所处理的公共数据是否存在。比如上述代码中的:
cond.wait(sbgurad, [this]{// 一个lambda表达式,相当于一个可调用对象。if(!msgRecvQueue.empty()) return true;else return false;});
我们就是通过if(!msgRecvQueue.empty())
来判断条件是否满足。