C++多线程__新__研二下
目录
4.2.2、 有读有写:读的时候不能写,写的时候不能读,可能存在线程1还没有对变量写完,线程2又对该变量去操作了
4.2.3 使用std::lock_guard()替代lock()和unlock()替代lock()和unock()
1、unique_lock()取代lock_guard(),unique_lock和lock_guard一样,在它们类的构造函数中加锁,在析构函数中解锁
2、std::adopt_lock 表示前面前面已经为mutex对象加锁,在unique_lock的构造函数中不需要再次加锁
3、std::try_to_lock 尝试加锁,如果加锁失败,可以做其他的事情,而不是阻塞住
七、条件变量 std::condition_varianle、wait()、notify_one()、notify_all()
(2)std::condition_variable类和该类中的wait()、notify_one()方法
(3)std::condition_variable类和该类中的wait()、notify_all()方法
一、并发、进程、线程的基本概念
1、并发:两个或者更多的任务(独立的活动)同时进行,一个程序同时执行多个独立的任务。以往计算机只是单核CPU,某一个时刻只能执行一个任务,由操作系统调度,每秒钟多次进行任务切换,不是真正的并发。现在计算机多是多核CPU,能够真正的并行执行多个任务(硬件并发)
2、可执行程序:磁盘上的文件,windows下扩展名为.exe的文件
3、进程:一个可执行程序运行起来就叫一个进程运行起来了,或者说进程就是运行起来了的可执行程序
4、线程:每个进程都有一个主线程(自动创建,一般是main函数),实际上运行程序的时候,实际上是该进程的主线程在运行,线程就是执行代码的一条道路,除了主线程之外,可以自己写代码创建其他线程,每创建一个新线程就可以多干一个不同的事,但是线程并不是越多越好(子线程最多不可超过300个),每个线程都需要一个独立的堆栈空间,线程之间的切换是要保存中间数据的,会耗费本该是程序运行的时间
总结线程:
a 线程是用来执行代码的
b 把线程理解为一个新的通路
c 一个进程自动包含一个主线程,主线程随着进程的自动启动和结束
d 多线程程序可以同时做多个事
二、并发的实现方法
a)多个进程实现并发
b)在单独的一个进程中,创建多个线程实现并发
1、多进程并发
world ie浏览器 爱奇艺等软件同时运行
进程之间的通信(同一台电脑)方法:管道、文件、消息队列等
进程之间的通信(不同电脑)方法:socket通信技术
2、多线程并发
每个线程都有自己独立的运行路径,且共享地址空间(共享内存)
全局变量、指针、引用都可以在不同线程之间传递,共享内存也胡存在一些问题,如数据一致性问题,例如线程1和线程2同时对一个变量进行操作时候,就会出现意外。
总结:线程如下优点
1)线程启动速度比进程快
2)系统资源开销更少
缺点:存在数据一致性问题
三、C++11新标准线程库(可以跨平台widows和Linux)
C++11增加了对多线程的支持,意味着可移植性
三、join和detach方法
1、join方法
程序运行起来,生成一个进程,该进程所在的主进程自动运行
自己创建的线程也需要从一个函数开始执行,如果这个函数执行完毕该线程也运行结束
一般情况下,如果主线程执行完毕,子线程还没有结束,那么整个子线程会被系统强行终止
所以一般情况下,如果想保持子线程的运行状态,就要让主线程一直保持运行
1、thread是标准库中的一个类
2、join阻塞主线程,让主线程等待子线程执行完毕,
实例:
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 4 using namespace std; 5 6 void myprint() 7 { 8 cout<<"我的线程开始执行"<<endl; 9 //可以做一些其他的事情 10 cout<<"我的线程执行结束"<<endl; 11 } 12 13 int main() 14 { 15 thread mytobj(myprint); //创建了线程,该线程执行起点为myprint(),该线程开始执行 16 17 //主线程可以做一些其他的事情,其中这些事情和myorint()函数是同时执行的 18 19 mytobj.join(); //主线程阻塞到这里,子线程继续执行,主线程等待子线程执行完毕,当子线程执行完毕,主线程就继续向下执行 20 21 22 cout<<"HelloWorld"<<endl; 23 24 return 0; 25 }
//如果不加join()此时这个代码有两条线同时在跑,打印"我的线程开始执行"和打印"HelloWorld"是通过不同线路来打印的
2、detach方法
传统主线程要等待子线程执行完毕再推出,但是主线程也可以和子线程分离,即主线程可以提前结束,以提高程序运行效率
一旦detach()之后,与这个主线程关联的线程对象就会失去了与主线程join的资格
此时子线程在后台运行,这个子线程被C++运行时库接管
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 4 using namespace std; 5 6 void myprint() 7 { 8 cout<<"我的线程开始执行"<<endl; 9 //可以做一些其他的事情 10 cout<<"我的线程执行结束1"<<endl; 11 cout<<"我的线程执行结束2"<<endl; 12 cout<<"我的线程执行结束3"<<endl; 13 cout<<"我的线程执行结束4"<<endl; 14 cout<<"我的线程执行结束5"<<endl; 15 cout<<"我的线程执行结束6"<<endl; 16 cout<<"我的线程执行结束7"<<endl; 17 cout<<"我的线程执行结束8"<<endl; 18 cout<<"我的线程执行结束9"<<endl; 19 cout<<"我的线程执行结束10"<<endl; 20 cout<<"我的线程执行结束11"<<endl; 21 } 22 23 int main() 24 { 25 thread mytobj(myprint); //创建了线程,该线程执行起点为myprint(),该线程开始执行 26 27 //主线程可以做一些其他的事情,其中这些事情和myorint()函数是同时执行的 28 29 mytobj.detach(); //主线程阻塞到这里,子线程继续执行,主线程等待子线程执行完毕,当子线程执行完毕,主线程就继续向下执行 30 31 32 cout<<"HelloWorld1"<<endl; 33 cout<<"HelloWorld2"<<endl; 34 cout<<"HelloWorld3"<<endl; 35 cout<<"HelloWorld4"<<endl; 36 cout<<"HelloWorld5"<<endl; 37 cout<<"HelloWorld6"<<endl; 38 cout<<"HelloWorld7"<<endl; 39 40 return 0; 41 }
打印可能是:
1 我的线程开始执行 2 我的线程执行结束1 3 我的线程执行结束2 4 HelloWorld1 5 HelloWorld2 6 HelloWorld3 7 HelloWorld4 8 我的线程执行结束8 9 我的线程执行结束9 10 我的线程执行结束3 11 HelloWorld5 12 HelloWorld6 13 HelloWorld7 14 执行完毕
//此时子线程和主线程同时执行,但是这样可能会存在主线内的代码执行完毕,但是子线程中的代码还没有执行完毕的现象
3、detachable():判断是否可以使用join或detach,返回true表示可以join,否则表示不可join
4、类对象也为可调用对象
1 例如: 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 5 using namespace std; 6 7 class TA 8 { 9 public: 10 void operator()(); //不能带参数 11 { 12 cout<<"子线程开始1"<<endl; 13 cout<<"子线程开始2"<<endl; 14 cout<<"子线程开始3"<<endl; 15 cout<<"子线程开始4"<<endl; 16 } 17 18 }; 19 20 void myprint() 21 { 22 cout<<"我的线程开始执行"<<endl; 23 //可以做一些其他的事情 24 cout<<"我的线程执行结束1"<<endl; 25 cout<<"我的线程执行结束2"<<endl; 26 cout<<"我的线程执行结束3"<<endl; 27 cout<<"我的线程执行结束4"<<endl; 28 cout<<"我的线程执行结束5"<<endl; 29 cout<<"我的线程执行结束6"<<endl; 30 cout<<"我的线程执行结束7"<<endl; 31 cout<<"我的线程执行结束8"<<endl; 32 cout<<"我的线程执行结束9"<<endl; 33 cout<<"我的线程执行结束10"<<endl; 34 cout<<"我的线程执行结束11"<<endl; 35 } 36 37 int main() 38 { 39 TA ta; 40 thread mytobj3(ta); //ta为可调用对象 类对象也为可调用对象 41 mytobj3.join(); //等待子线程执行结束,也可以用detach() 42 43 //主线程可以做一些其他的事情,其中这些事情和myorint()函数是同时执行的 44 45 cout<<"HelloWorld1"<<endl; 46 47 return 0; 48 }
5、类中的私有数据含有引用或者是指针时候会出现意外
如下代码:
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 4 using namespace std; 5 6 class TA 7 { 8 int & m_i; 9 public: 10 TA(int & i):m_i(i); 11 void operator()() //不能带参数 12 { 13 cout<<"m_i的值为"<<m_i<<endl; 14 cout<<"m_i的值为"<<m_i<<endl; 15 cout<<"m_i的值为"<<m_i<<endl; 16 cout<<"m_i的值为"<<m_i<<endl; 17 cout<<"m_i的值为"<<m_i<<endl; 18 cout<<"m_i的值为"<<m_i<<endl; 19 cout<<"m_i的值为"<<m_i<<endl; 20 21 } 22 23 }; 24 25 void myprint() 26 { 27 cout<<"我的线程开始执行"<<endl; 28 //可以做一些其他的事情 29 cout<<"我的线程执行结束1"<<endl; 30 cout<<"我的线程执行结束2"<<endl; 31 cout<<"我的线程执行结束3"<<endl; 32 cout<<"我的线程执行结束4"<<endl; 33 cout<<"我的线程执行结束5"<<endl; 34 cout<<"我的线程执行结束6"<<endl; 35 cout<<"我的线程执行结束7"<<endl; 36 cout<<"我的线程执行结束8"<<endl; 37 cout<<"我的线程执行结束9"<<endl; 38 cout<<"我的线程执行结束10"<<endl; 39 cout<<"我的线程执行结束11"<<endl; 40 } 41 42 int main() 43 { 44 int myi=6; 45 TA ta(myi); 46 thread mytobj3(ta); 47 mytobj3.detach(); 48 49 //主线程可以做一些其他的事情,其中这些事情和myorint()函数是同时执行的 50 51 cout<<"HelloWorld1"<<endl; 52 53 return 0; 54 }
主线程先执行完,子线程还在执行,myi是主线程的变量,主线程执行完myi就会被销毁,子线程再去使用myi的时候就会出错
或者将类TA中的公有数据改成不是引用也是可以的,这样就会使用主线程中myi的拷贝,这样就没有问题了
主线程结束后,ta也会被销毁,但是ta不在没有关系,ta是会被复制到了子线程中去的(所以在类中要有复制构造函数),所以不会因此出错,如下例程:
只要类中私有数据没有引用、指针,使用detach()就没有问题
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 4 using namespace std; 5 6 class TA 7 { 8 private: 9 int m_i; 10 public: 11 TA(int & i) :m_i(i) 12 { 13 cout << "构造函数被执行" << endl; 14 } 15 TA(const TA & ta) :m_i(ta.m_i) 16 { 17 cout << "复制构造函数被执行" << endl; 18 } 19 ~TA() 20 { 21 cout << "析构函数被执行" << endl; 22 } 23 24 void operator()() //不能带参数 25 { 26 cout << "m_i1的值为" << m_i << endl; 27 cout << "m_i2的值为" << m_i << endl; 28 cout << "m_i3的值为" << m_i << endl; 29 cout << "m_i4的值为" << m_i << endl; 30 cout << "m_i5的值为" << m_i << endl; 31 cout << "m_i6的值为" << m_i << endl; 32 cout << "m_i7的值为" << m_i << endl; 33 } 34 35 }; 36 37 int main() 38 { 39 int myi = 6; 40 TA ta(myi); 41 thread mytobj3(ta); 42 mytobj3.join(); 43 44 //主线程可以做一些其他的事情,其中这些事情和myorint()函数是同时执行的 45 46 cout << "HelloWorld1" << endl; 47 48 system("pause"); 49 return 0; 50 }
执行结果:
但是主线程中的类对象ta执行析构函数没有显示不知道为啥。。注:以上方法也可以改为detach()
6、用Lamda表达式创建子线程(此处只写出了主函数)
1 int main() 2 { 3 auto mylambdathread = [] 4 { 5 cout<<"我的子线程开始执行"<<endl; 6 } 7 thread mytobj(mylambdathread); 8 mytobj.join(); 9 10 return 0; 11 }
四、 共享数据的保护问题
1、创建多个线程
01)多个线程的执行顺序是乱的,有可能线程1还没有结束,就去执行线程2了,这个和系统的运行调度机制有关
02)子线程等待所有子线程结束,主线程结束(使用join方法)
03)把thread对象放到容器里面,这对创建大量线程并对大量线程管理很方便
1 //创建多个线程 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <vector> 5 6 using namespace std; 7 8 //线程入口函数,但是可以给多个线程使用 9 void myprint(int value) 10 { 11 cout << "子线程开始执行,编号=" << value << endl; 12 13 //可以做一些其他的事情 14 15 cout << "子线程执行结束,编号=" << value << endl; 16 } 17 18 int main() 19 { 20 vector <thread> mythreads; 21 22 for (int i = 0; i < 10; i++) 23 { 24 mythreads.push_back(thread(myprint, i)); //创建十个线程,并开始执行,其中i作为实参传入myprint()中 25 } 26 for (auto iter = mythreads.begin(); iter != mythreads.end(); iter++) 27 { 28 iter->join(); //等待十个线程执行完毕 29 } 30 31 cout << "HelloWorld" << endl; 32 33 system("pause"); 34 return 0; 35 }
执行结果1:
2、 数据共享问题分析
4.2.1、只读数据:不去给数据重新赋值,此时是没有问题的
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 4 using namespace std; 5 6 vector<int> g_v = {1,2,3}; //多线程共享数据 7 8 //myprint()只读了g_v的数据,每个线程并没有去给g_V写入数据 9 void myprint(int value) 10 { 11 cout<<"id为:"<<std::this_thread::get_id()<<"的g_v的值为"<<g_v[1]<<","<<g_v[2]<<endl; 12 } 13 14 int main() 15 { 16 vector <thread> mythreads; 17 18 for(int i=0;i<10;i++) 19 { 20 mythreads.push_back(myprint,i); //创建十个线程,并开始执行 21 } 22 for(auto iter=mythreads.begin();iter!=mythreads.end();iter++) 23 { 24 iter->join(); //等待十个线程执行完毕 25 } 26 27 cout<<"HelloWorld"<<endl; 28 29 system("pause"); 30 return 0; 31 }
4.2.2、 有读有写:读的时候不能写,写的时候不能读,可能存在线程1还没有对变量写完,线程2又对该变量去操作了
数据案例保护案例:
开发一个网络服务器,有啷个线程,线程1:收集玩家命令,线程2:从队列中取出命令并解析
假定玩家发的命令为一个数字,list也是一个容器,list在频繁的删除和插入数据效率较高,vector随机插入和删除效率较高
(1)互斥量:来解决多线程共享数据的保护问题
锁:某个线程用代码把共享数据锁住,其他线程如果想操作共享数据,必须等待解锁
互斥量的基本概念:一个类对象,理解成一把锁,多个线程使用lock(),只有一个线程锁住成功,如果没有锁成功,那么就会卡在lock()这里,
只保护需要保护的数据,少l起不到保护效果,多了影响效率
(2)lock()和unlock()的使用
步骤:先lock()、操作共享数据(读写)、unlock()
lock()和unlock()要成对使用
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 #include <list> 4 #include <mutex> //lock() 5 6 using namespace std; 7 8 class A 9 { 10 public: 11 //把收到的信息(玩家命令)放入到一个队列的线程 12 void inMsgRecvQueue() 13 { 14 for (int i = 0; i < 100000; i++) 15 { 16 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 17 my_mutex.lock(); //如果线程2中的outMsgLULProc()被锁住,那么这里就不会被锁住,继续执行lock()下面的代码 18 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 19 my_mutex.unlock(); //只有在对msgRecvQueue写数据的这一句需要保护,其余不需要保护,如果把for循环全保护,那么效率就会降低 20 } 21 } 22 bool outMsgLULProc(int &command) 23 { 24 //如果线程1中的inMsgRecvQueue()被锁住,那么这里就不会被锁住,继续执行lock()下面的代码 25 my_mutex.lock(); //使用同一把锁对线程2也加锁 26 if (!msgRecvQueue.empty()) 27 { 28 //消息不为空 29 command = msgRecvQueue.front(); 30 msgRecvQueue.pop_front(); //移除取出的元素 31 //以下可以考虑处理数据... 32 my_mutex.unlock(); //每一个分支都得由unlock() 33 return true; 34 } 35 else 36 { 37 my_mutex.unlock(); //每一个分支都得由unlock() 38 return false; 39 } 40 } 41 42 //把数据从消息队列中取出的线程 43 void outMsgRecvQueue() 44 { 45 int command = 0; 46 47 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 48 { 49 bool result = outMsgLULProc(command); 50 if (result == true) 51 { 52 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 53 //以下可以考虑处理数据 54 } 55 } 56 } 57 58 private: 59 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 60 std::mutex my_mutex; //创建一个互斥量 61 }; 62 int main() 63 { 64 A myobja; //生成类对象 65 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 66 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 67 myOutMsgObj.join(); 68 myInMsgObj.join(); 69 70 system("pause"); 71 return 0; 72 }
线程1中的inMsgRecvQueue()会操作共享数据,线程2中的outMsgRecvQueue()也会操作共享数据,此时就需要对两个线程中操作共享数据的那句代码分别加锁和解锁
但是那个线程先加锁成功是由系统决定的,假如线程1中的inMsgRecvQueue()先加所锁成功(会继续执行lock()以下的代码),那么线程2中的outMsgRecvQueue()就不
会加锁成功(即卡在lock(),程序不会向下执行outMsgRecvQueue()中lock()以下的代码)
4.2.3 使用std::lock_guard()替代lock()和unlock()替代lock()和unock()
C++使用std::lock_guard()可以同时取代lock()和unlock()
原理是lock_guard()是一个类模板,在该类中的构造函数中使用了lock(),在该类的析构函数中使用了unlock()
由于一般是在一个调用函数中使用lock_guard(),即会创建局部类对象,那么在该调用函数结束时就会调用lock_guard()类对象的析构函数
但是lock_guard()也有缺点,就是想当于在调用函数的最后使用unlock(),解决方法是使用大括号让lock_guard()提前结束生命周期
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 #include <list> 4 #include <mutex> //lock() 5 6 using namespace std; 7 8 class A 9 { 10 public: 11 //把收到的信息(玩家命令)放入到一个队列的线程 12 void inMsgRecvQueue() 13 { 14 for (int i = 0; i < 100000; i++) 15 { 16 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 17 std::lock_guard<std::mutex> sbguard(my_mutex); //sbguard为lock_guard的类对象,my_mutex为在private中定义的互斥量 18 //my_mutex.lock(); 19 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 20 //my_mutex.unlock(); //只有在对msgRecvQueue写数据的这一句需要保护,其余不需要保护,如果把for循环全保护,那么效率就会降低 21 } 22 } 23 bool outMsgLULProc(int &command) 24 { 25 std::lock_guard<std::mutex> sbguard(my_mutex); //sbguard为lock_guard的类对象,my_mutex为在private中定义的互斥量 26 if (!msgRecvQueue.empty()) 27 { 28 //消息不为空 29 command = msgRecvQueue.front(); 30 msgRecvQueue.pop_front(); //移除取出的元素 31 //以下可以考虑处理数据... 32 //my_mutex.unlock(); //使用lock_guard就不必再使用lock()和unlock() 33 return true; 34 } 35 else 36 { 37 //my_mutex.unlock(); //使用lock_guard就不必再使用lock()和unlock() 38 return false; 39 } 40 } 41 42 //把数据从消息队列中取出的线程 43 void outMsgRecvQueue() 44 { 45 int command = 0; 46 47 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 48 { 49 bool result = outMsgLULProc(command); 50 if (result == true) 51 { 52 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 53 //以下可以考虑处理数据 54 } 55 } 56 } 57 58 private: 59 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 60 std::mutex my_mutex; //创建一个互斥量 61 }; 62 int main() 63 { 64 A myobja; //生成类对象 65 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 66 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 67 myOutMsgObj.join(); 68 myInMsgObj.join(); 69 70 system("pause"); 71 return 0; 72 }
4.2.4、死锁(至少有两个互斥量,即两把锁)
一个互斥量是一把锁
线程A、线程B 锁1、锁2
线程A把锁1lock()成功,那么就会继续执行下面的代码,然后线程A又去lock()锁2,但是还没有lock()成功,突然因为线程调度,线程B开始执行,
因为线程A已经把锁1lock成功了,所以线程B肯定不会将锁1lock成功,线程B先lock()锁2,且lock成功(因为线程A还没有把锁2lock成功),之后线程2
要去lock锁1,此时死锁就发生了。
此时的情况是:
线程Alock着锁1不松手(对锁1没有unlock()),且卡在了锁2那里,后面的代码不可执行下去;
线程Block着锁2不松手(对锁2没有unlock()),且卡在了锁1那里,后面的代码不可执行下去。
线程1先lock锁1再lock锁2
线程2先lock锁2再lock锁1
就会出现死锁的现象
1 //线程1先lock锁1再lock锁2 2 //线程2先lock锁2再lock锁1 3 //就会出现死锁的现象 4 #include <iostream> 5 #include <thread> //尖括号表示系统头文件 6 #include <list> 7 #include <mutex> //lock() 8 9 using namespace std; 10 11 class A 12 { 13 public: 14 //把收到的信息(玩家命令)放入到一个队列的线程 15 void inMsgRecvQueue() 16 { 17 for (int i = 0; i < 100000; i++) 18 { 19 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 20 //可以加上大括号让lock_guard()提前结束生命周期 21 my_mutex1.lock(); 22 //中间可能隔了很多代码(需要保护不同的数据块) 23 my_mutex2.lock(); 24 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 25 my_mutex1.unlock(); //只有在对msgRecvQueue写数据的这一句需要保护,其余不需要保护,如果把for循环全保护,那么效率就会降低 26 my_mutex2.unlock(); 27 } 28 } 29 bool outMsgLULProc(int &command) 30 { 31 if (!msgRecvQueue.empty()) 32 { 33 //消息不为空 34 my_mutex2.lock(); 35 my_mutex1.lock(); 36 command = msgRecvQueue.front(); 37 msgRecvQueue.pop_front(); //移除取出的元素 38 //以下可以考虑处理数据... 39 my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 40 my_mutex2.unlock(); 41 return true; 42 } 43 else 44 { 45 //先解锁哪个都是可以的 46 my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 47 my_mutex2.unlock(); 48 return false; 49 } 50 } 51 52 //把数据从消息队列中取出的线程 53 void outMsgRecvQueue() 54 { 55 int command = 0; 56 57 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 58 { 59 bool result = outMsgLULProc(command); 60 if (result == true) 61 { 62 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 63 //以下可以考虑处理数据 64 } 65 } 66 } 67 68 private: 69 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 70 std::mutex my_mutex1; //创建一个互斥量1 71 std::mutex my_mutex2; //创建一个互斥量2 72 }; 73 int main() 74 { 75 A myobja; //生成类对象 76 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 77 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 78 myOutMsgObj.join(); 79 myInMsgObj.join(); 80 81 system("pause"); 82 return 0; 83 }
4.2.5、死锁的解决方法
方法一:使用lock()和unlock(),且保证不能线程之间对互斥量lock()的顺序一致
只要保证两个互斥量(锁)lock()的顺序一致就不会出现死锁的现象
使用lock_guard也是要保证两个线程lock_guard()两个互斥量的顺序是 一样的即可
比如线程1中:
std::lock_guard<std::mutex> sbguard(my_mutex1);
std::lock_guard<std::mutex> sbguard(my_mutex2);
那么线程2中的顺序也得是:
std::lock_guard<std::mutex> sbguard(my_mutex1);
std::lock_guard<std::mutex> sbguard(my_mutex2);
1 //上面出现死锁的解决方法:把线程1和线程2lock()两个互斥量的顺序改成一致即可 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 //for (int i = 0; i < 100000; i++) 16 //{ 17 cout << "inMsgRecvQueue执行,插入一个数据:" << 1 << endl; 18 my_mutex1.lock(); 19 cout << "inMsgRecvQueue()中的my_mutex1已经被锁住" << endl; 20 //中间可能隔了很多代码(需要保护不同的数据块) 21 my_mutex2.lock(); 22 cout << "inMsgRecvQueue()中的my_mutex2已经被锁住" << endl; 23 msgRecvQueue.push_back(1); //假设数字i就是收到的玩家命令 24 my_mutex1.unlock(); //只有在对msgRecvQueue写数据的这一句需要保护,其余不需要保护,如果把for循环全保护,那么效率就会降低 25 cout << "inMsgRecvQueue()中的my_mutex1已经被解锁" << endl; 26 my_mutex2.unlock(); 27 cout << "inMsgRecvQueue()中的my_mutex2已经被解锁" << endl; 28 //} 29 } 30 bool outMsgLULProc(int &command) 31 { 32 if (!msgRecvQueue.empty()) 33 { 34 //消息不为空 35 my_mutex1.lock(); //这里必须和inMsgRecvQueue()中锁my_mutex1和my_mutex2的顺序是一样的 36 //cout << "outMsgLULProc(int &command)中的my_mutex1已经被锁住" << endl; 37 my_mutex2.lock(); 38 cout << "outMsgLULProc(int &command)中的my_mutex2已经被锁住" << endl; 39 command = msgRecvQueue.front(); 40 msgRecvQueue.pop_front(); //移除取出的元素 41 //以下可以考虑处理数据... 42 my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 43 cout << "outMsgLULProc(int &command)中的my_mutex1已经被解锁" << endl; 44 my_mutex2.unlock(); 45 cout << "outMsgLULProc(int &command)中的my_mutex1已经被解锁" << endl; 46 return true; 47 } 48 else 49 { 50 //先解锁哪个都是可以的 51 my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 52 my_mutex2.unlock(); 53 return false; 54 } 55 } 56 57 //把数据从消息队列中取出的线程 58 void outMsgRecvQueue() 59 { 60 int command = 0; 61 62 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 63 { 64 bool result = outMsgLULProc(command); 65 if (result == true) 66 { 67 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 68 //以下可以考虑处理数据 69 } 70 } 71 } 72 73 private: 74 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 75 std::mutex my_mutex1; //创建一个互斥量1 76 std::mutex my_mutex2; //创建一个互斥量2 77 }; 78 int main() 79 { 80 A myobja; //生成类对象 81 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 82 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 83 myOutMsgObj.join(); 84 myInMsgObj.join(); 85 86 system("pause"); 87 return 0; 88 }
这样还是有问题的,出现unlock of unknowed mutex,刚刚百度了一下,可能的原因是:
(函数1)unlock,(函数2)lock,(函数1)delete,(函数2)unlock。
方法二:使用lock_guard(),且保证不能线程之间对互斥量lock()的顺序一致
1 //使用lock_guard()可以解决上述死锁的问题,也就是要保证线程1和线程2中的两个互斥量lock_guard()的顺一致 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 for (int i = 0; i < 100000; i++) 16 { 17 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 18 std::lock_guard<std::mutex> sbguard1(my_mutex1); 19 std::lock_guard<std::mutex> sbguard2(my_mutex2); 20 //my_mutex1.lock(); 21 //中间可能隔了很多代码(需要保护不同的数据块) 22 //my_mutex2.lock(); 23 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 24 //my_mutex1.unlock(); //只有在对msgRecvQueue写数据的这一句需要保护,其余不需要保护,如果把for循环全保护,那么效率就会降低 25 //my_mutex2.unlock(); 26 } 27 } 28 bool outMsgLULProc(int &command) 29 { 30 if (!msgRecvQueue.empty()) 31 { 32 //消息不为空 33 //my_mutex1.lock(); //这里必须和inMsgRecvQueue()中锁my_mutex1和my_mutex2的顺序是一样的 34 //my_mutex2.lock(); 35 std::lock_guard<std::mutex> sbguard1(my_mutex1); 36 std::lock_guard<std::mutex> sbguard2(my_mutex2); 37 command = msgRecvQueue.front(); 38 msgRecvQueue.pop_front(); //移除取出的元素 39 //以下可以考虑处理数据... 40 //my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 41 //my_mutex2.unlock(); 42 return true; 43 } 44 else 45 { 46 //先解锁哪个都是可以的 47 //my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 48 //my_mutex2.unlock(); 49 return false; 50 } 51 } 52 53 //把数据从消息队列中取出的线程 54 void outMsgRecvQueue() 55 { 56 int command = 0; 57 58 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 59 { 60 bool result = outMsgLULProc(command); 61 if (result == true) 62 { 63 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 64 //以下可以考虑处理数据 65 } 66 } 67 } 68 69 private: 70 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 71 std::mutex my_mutex1; //创建一个互斥量1 72 std::mutex my_mutex2; //创建一个互斥量2 73 }; 74 int main() 75 { 76 A myobja; //生成类对象 77 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 78 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 79 myOutMsgObj.join(); 80 myInMsgObj.join(); 81 82 system("pause"); 83 return 0; 84 }
方法三:使用std::lock()类模板
能力:同时锁住两个或者是连个以上的互斥量,不存在出现死锁的问题
std::lock():如果锁1没有锁住,那么就等待在那里,同时解锁已经锁住了的互斥量,等到锁1锁住了的时候,再去锁住已经解锁了的互斥量
使用方法:
std::lock(mutex1,mutex2,mutex3); //相当于每个互斥量都调用了lock()
解锁时同时对mutex1,mutex2,mutex3同时解锁
my_mutex1.unlock()
my_mutex2.unlock()
my_mutex3.unlock()
使用std::lock()类模板互斥量的顺序可以不一样,如
在线程1中:
std::lock(mutex1,mutex2);
my_mutex1.unlock()
my_mutex2.unlock()
在线程2中可以是:
std::lock(mutex2,mutex1);
my_mutex1.unlock()
my_mutex2.unlock()
1 //使用lock()类模板解决上述死锁的问题 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 for (int i = 0; i < 100000; i++) 16 { 17 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 18 std::lock(my_mutex1, my_mutex2); 19 //std::lock_guard<std::mutex> sbguard1(my_mutex1); 20 //std::lock_guard<std::mutex> sbguard2(my_mutex2); 21 //my_mutex1.lock(); 22 //中间可能隔了很多代码(需要保护不同的数据块) 23 //my_mutex2.lock(); 24 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 25 my_mutex1.unlock(); //只有在对msgRecvQueue写数据的这一句需要保护,其余不需要保护,如果把for循环全保护,那么效率就会降低 26 my_mutex2.unlock(); 27 } 28 } 29 bool outMsgLULProc(int &command) 30 { 31 if (!msgRecvQueue.empty()) 32 { 33 //消息不为空 34 //my_mutex1.lock(); //这里必须和inMsgRecvQueue()中锁my_mutex1和my_mutex2的顺序是一样的 35 //my_mutex2.lock(); 36 std::lock(my_mutex1, my_mutex2); 37 //std::lock_guard<std::mutex> sbguard1(my_mutex1); 38 //std::lock_guard<std::mutex> sbguard2(my_mutex2); 39 command = msgRecvQueue.front(); 40 msgRecvQueue.pop_front(); //移除取出的元素 41 //以下可以考虑处理数据... 42 my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 43 my_mutex2.unlock(); 44 return true; 45 } 46 else 47 { 48 //先解锁哪个都是可以的 49 my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 50 my_mutex2.unlock(); 51 return false; 52 } 53 } 54 55 //把数据从消息队列中取出的线程 56 void outMsgRecvQueue() 57 { 58 int command = 0; 59 60 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 61 { 62 bool result = outMsgLULProc(command); 63 if (result == true) 64 { 65 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 66 //以下可以考虑处理数据 67 } 68 } 69 } 70 71 private: 72 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 73 std::mutex my_mutex1; //创建一个互斥量1 74 std::mutex my_mutex2; //创建一个互斥量2 75 }; 76 int main() 77 { 78 A myobja; //生成类对象 79 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 80 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 81 myOutMsgObj.join(); 82 myInMsgObj.join(); 83 84 system("pause"); 85 return 0; 86 }
这个也不是很好用。。。
方法五:将std::lock()类模板和lock_guard()结合使用,可以不适用unlock()
在线程1中:
std::lock(mutex1,mutex2);
std::lock_guard<std::mutex> sbguard1(my_mutex1,std::adopt_lock());
std::lock_guard<std::mutex> sbguard2(my_mutex2,std::adopt_lock());
在线程2中:
std::lock(mutex1,mutex2);
std::lock_guard<std::mutex> sbguard1(my_mutex1,std::adopt_lock());
std::lock_guard<std::mutex> sbguard2(my_mutex2,std::adopt_lock());
std::adopt_lock()是一个类对象,表示不需要对std::lock_guard<std::mutex> sbguard1(my_mutex1,std::adopt_lock())中的my_mutex1再次lock()
1 //使用lock()类模板和lock_guard()解决上述死锁的问题 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 for (int i = 0; i < 100000; i++) 16 { 17 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 18 std::lock(my_mutex1, my_mutex2); 19 std::lock_guard<std::mutex> sbguard1(my_mutex1,std::adopt_lock); 20 std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock); 21 //中间可能隔了很多代码(需要保护不同的数据块) 22 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 23 //my_mutex1.unlock(); //只有在对msgRecvQueue写数据的这一句需要保护,其余不需要保护,如果把for循环全保护,那么效率就会降低 24 //my_mutex2.unlock(); 25 } 26 } 27 bool outMsgLULProc(int &command) 28 { 29 if (!msgRecvQueue.empty()) 30 { 31 //消息不为空 32 //my_mutex1.lock(); //这里必须和inMsgRecvQueue()中锁my_mutex1和my_mutex2的顺序是一样的 33 //my_mutex2.lock(); 34 std::lock(my_mutex1, my_mutex2); 35 std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock); 36 std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock); 37 command = msgRecvQueue.front(); 38 msgRecvQueue.pop_front(); //移除取出的元素 39 //以下可以考虑处理数据... 40 //my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 41 //my_mutex2.unlock(); 42 return true; 43 } 44 else 45 { 46 //先解锁哪个都是可以的 47 //my_mutex1.unlock(); //使用lock_guard就不必再使用lock()和unlock() 48 //my_mutex2.unlock(); 49 return false; 50 } 51 } 52 53 //把数据从消息队列中取出的线程 54 void outMsgRecvQueue() 55 { 56 int command = 0; 57 58 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 59 { 60 bool result = outMsgLULProc(command); 61 if (result == true) 62 { 63 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 64 //以下可以考虑处理数据 65 } 66 } 67 } 68 69 private: 70 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 71 std::mutex my_mutex1; //创建一个互斥量1 72 std::mutex my_mutex2; //创建一个互斥量2 73 }; 74 int main() 75 { 76 A myobja; //生成类对象 77 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 78 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 79 myOutMsgObj.join(); 80 myInMsgObj.join(); 81 82 system("pause"); 83 return 0; 84 }
这个运行是没有问题的
五、unique_lock()的讲解
1、unique_lock()取代lock_guard(),unique_lock和lock_guard一样,在它们类的构造函数中加锁,在析构函数中解锁
unique_lock()是一个类模板,工作中一般使用unique_lock(),但是占用空间比较大
lock_guard()和unique_lock()都是对互斥量加锁和解锁
1 //使用unique_lock()类模板和lock_guard()解决上述死锁的问题 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 for (int i = 0; i < 100000; i++) 16 { 17 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 18 std::unique_lock<std::mutex> sbguard1(my_mutex1); 19 std::unique_lock<std::mutex> sbguard2(my_mutex2); 20 //中间可能隔了很多代码(需要保护不同的数据块) 21 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 22 } 23 } 24 bool outMsgLULProc(int &command) 25 { 26 if (!msgRecvQueue.empty()) 27 { 28 //消息不为空 29 std::unique_lock<std::mutex> sbguard1(my_mutex1); 30 std::unique_lock<std::mutex> sbguard2(my_mutex2); 31 command = msgRecvQueue.front(); 32 msgRecvQueue.pop_front(); //移除取出的元素 33 //以下可以考虑处理数据... 34 return true; 35 } 36 else 37 { 38 //先解锁哪个都是可以的 39 return false; 40 } 41 } 42 43 //把数据从消息队列中取出的线程 44 void outMsgRecvQueue() 45 { 46 int command = 0; 47 48 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 49 { 50 bool result = outMsgLULProc(command); 51 if (result == true) 52 { 53 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 54 //以下可以考虑处理数据 55 } 56 } 57 } 58 59 private: 60 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 61 std::mutex my_mutex1; //创建一个互斥量1 62 std::mutex my_mutex2; //创建一个互斥量2 63 }; 64 int main() 65 { 66 A myobja; //生成类对象 67 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 68 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 69 myOutMsgObj.join(); 70 myInMsgObj.join(); 71 72 system("pause"); 73 return 0; 74 }
2、std::adopt_lock 表示前面前面已经为mutex对象加锁,在unique_lock的构造函数中不需要再次加锁
std::adopt_lock:表示互斥量已经提前被lock了,不需要使用lock_guard()和unique_lock()再次lock了
1 //使用unique_lock()类模板解决上述死锁的问题 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 for (int i = 0; i < 100000; i++) 16 { 17 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 18 my_mutex1.lock(); 19 my_mutex2.lock(); 20 std::unique_lock<std::mutex> sbguard1(my_mutex1,std::adopt_lock); 21 std::unique_lock<std::mutex> sbguard2(my_mutex2, std::adopt_lock); 22 //中间可能隔了很多代码(需要保护不同的数据块) 23 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 24 } 25 } 26 bool outMsgLULProc(int &command) 27 { 28 if (!msgRecvQueue.empty()) 29 { 30 //消息不为空 31 my_mutex1.lock(); 32 my_mutex2.lock(); 33 std::unique_lock<std::mutex> sbguard1(my_mutex1, std::adopt_lock); 34 std::unique_lock<std::mutex> sbguard2(my_mutex2, std::adopt_lock); 35 command = msgRecvQueue.front(); 36 msgRecvQueue.pop_front(); //移除取出的元素 37 //以下可以考虑处理数据... 38 return true; 39 } 40 else 41 { 42 //先解锁哪个都是可以的 43 return false; 44 } 45 } 46 47 //把数据从消息队列中取出的线程 48 void outMsgRecvQueue() 49 { 50 int command = 0; 51 52 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 53 { 54 bool result = outMsgLULProc(command); 55 if (result == true) 56 { 57 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 58 //以下可以考虑处理数据 59 } 60 } 61 } 62 63 private: 64 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 65 std::mutex my_mutex1; //创建一个互斥量1 66 std::mutex my_mutex2; //创建一个互斥量2 67 }; 68 int main() 69 { 70 A myobja; //生成类对象 71 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 72 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 73 myOutMsgObj.join(); 74 myInMsgObj.join(); 75 76 system("pause"); 77 return 0; 78 }
3、std::try_to_lock 尝试加锁,如果加锁失败,可以做其他的事情,而不是阻塞住
引入实例:
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 #include <list> 4 #include <mutex> //lock() 5 6 using namespace std; 7 8 class A 9 { 10 public: 11 //把收到的信息(玩家命令)放入到一个队列的线程 12 void inMsgRecvQueue() 13 { 14 for (int i = 0; i < 100000; i++) 15 { 16 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 17 std::unique_lock<std::mutex> sbguard1(my_mutex1,std::adopt_lock); 18 //std::unique_lock<std::mutex> sbguard2(my_mutex2, std::adopt_lock); 19 //中间可能隔了很多代码(需要保护不同的数据块) 20 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 21 } 22 } 23 bool outMsgLULProc(int &command) 24 { 25 if (!msgRecvQueue.empty()) 26 { 27 //消息不为空 28 std::unique_lock<std::mutex> sbguard1(my_mutex1); 29 //std::unique_lock<std::mutex> sbguard2(my_mutex2); 30 31 std::chrono::milliseconds dura(20000); //20000毫秒(20s) 32 std::this_thread::sleep_for(dura); //休息20s 33 34 command = msgRecvQueue.front(); 35 msgRecvQueue.pop_front(); //移除取出的元素 36 //以下可以考虑处理数据... 37 return true; 38 } 39 else 40 { 41 //先解锁哪个都是可以的 42 return false; 43 } 44 } 45 46 //把数据从消息队列中取出的线程 47 void outMsgRecvQueue() 48 { 49 int command = 0; 50 51 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 52 { 53 bool result = outMsgLULProc(command); 54 if (result == true) 55 { 56 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 57 //以下可以考虑处理数据 58 } 59 } 60 } 61 62 private: 63 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 64 std::mutex my_mutex1; //创建一个互斥量1 65 std::mutex my_mutex2; //创建一个互斥量2 66 }; 67 int main() 68 { 69 A myobja; //生成类对象 70 //由于是先创建的outMsgRecvQueue()线程,所以应该是outMsgRecvQueue()线程先跑起来 71 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 72 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 73 myOutMsgObj.join(); 74 myInMsgObj.join(); 75 76 system("pause"); 77 return 0; 78 }
问题:
outMsgRecvQueue()线程在锁住线程my_mutex1后,sleep了20s,此时inMsgRecvQueue()线程被阻塞住,也不能执行
由于是先创建的outMsgRecvQueue()线程,所以应该是outMsgRecvQueue()线程先跑起来,然后outMsgRecvQueue()把
my_mutex1先锁起来,然后进入sleep,此时进入inMsgRecvQueue(),但是会被阻塞在my_mutex1互斥量上,即一个线程卡20s
则另外一个线程也卡20s
解决方法:使用try_to_lock,这样即使inMsgRecvQueue()没有拿到my_mutex1的锁,也可以做else中的事情,而不是阻塞住
1 std::unique_lock<std::mutex> sbguard1(my_mutex1,std::try_to_lock); 2 if (sbguard1.owns_lock()) 3 { 4 //拿到了锁 5 msgRecvQueue.push_back(i); 6 //处理其他代码 7 } 8 else 9 { 10 //没拿到锁,可以在else里面做一些其他的事情 11 cout << "inMsgRecvQueue()执行,但没有拿到锁,只能做点其他的事" << endl; 12 }
1 #include <iostream> 2 #include <thread> //尖括号表示系统头文件 3 #include <list> 4 #include <mutex> //lock() 5 6 using namespace std; 7 8 class A 9 { 10 public: 11 //把收到的信息(玩家命令)放入到一个队列的线程 12 void inMsgRecvQueue() 13 { 14 for (int i = 0; i < 100000; i++) 15 { 16 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 17 std::unique_lock<std::mutex> sbguard1(my_mutex1,std::try_to_lock); 18 if (sbguard1.owns_lock()) 19 { 20 //拿到了锁 21 msgRecvQueue.push_back(i); 22 //处理其他代码 23 } 24 else 25 { 26 //没拿到锁,可以在else里面做一些其他的事情 27 cout << "inMsgRecvQueue()执行,但没有拿到锁,只能做点其他的事" << endl; 28 } 29 } 30 } 31 bool outMsgLULProc(int &command) 32 { 33 if (!msgRecvQueue.empty()) 34 { 35 //消息不为空 36 std::unique_lock<std::mutex> sbguard1(my_mutex1); 37 //std::unique_lock<std::mutex> sbguard2(my_mutex2); 38 39 std::chrono::milliseconds dura(1000); //1000毫秒(1s) 40 std::this_thread::sleep_for(dura); //休息1s 41 42 command = msgRecvQueue.front(); 43 msgRecvQueue.pop_front(); //移除取出的元素 44 //以下可以考虑处理数据... 45 return true; 46 } 47 else 48 { 49 //先解锁哪个都是可以的 50 return false; 51 } 52 } 53 54 //把数据从消息队列中取出的线程 55 void outMsgRecvQueue() 56 { 57 int command = 0; 58 59 for (int i = 0; i < 100000; i++) //加for循环的目的是为了看的更清楚 60 { 61 bool result = outMsgLULProc(command); 62 if (result == true) 63 { 64 cout << "outMsgRecvQueue()执行,取出一个元素:" << command << endl; 65 //以下可以考虑处理数据 66 cout << "helloWorld" << endl; 67 } 68 } 69 } 70 71 private: 72 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 73 std::mutex my_mutex1; //创建一个互斥量1 74 std::mutex my_mutex2; //创建一个互斥量2 75 }; 76 int main() 77 { 78 A myobja; //生成类对象 79 //由于是先创建的outMsgRecvQueue()线程,所以应该是outMsgRecvQueue()线程先跑起来 80 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 81 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 82 myOutMsgObj.join(); 83 myInMsgObj.join(); 84 85 system("pause"); 86 return 0; 87 } 88 89 //outMsgRecvQueue()拿到锁的机会可能比较小
执行很多次inMsgRecvQueue()中else语句中的东西,才会执行一次outMsgLULProc()
4、std::defer_lock 初始化一个不加锁的mutex对象,接下来可以使用unique_lock类对象的成员函数lock()对mutex对象加锁
自己前面也不可以先lock()
std::defer_lock就是不给mymutex1加锁,初始化一个没有加锁的mutex,
没有加锁的mutex的作用是可以灵活的调用unique_lock()的成员函数
std::unique_lock<std::mutex> sbguard1(my_mutex1,std::defer_lock); //只是初始化my_mutex1,将unique_lock类对象sbguard1和my_mutex1绑定,但并不给my_mutex1加锁 sbguard1.lock(); //此时可以不用unlock(),但是加上也没错,更加的灵活而已
5、unique_lock()的成员函数
1 std::unique_lock<std::mutex> sbguard1(my_mutex1,std::defer_lock); //创建一个没有加锁的my_mutex1,只是将sbguard1和my_mutex1绑定 2 sbguard1.lock(); //此时不用unlock()
但是也有sbguard1.unlock();因为有一些非共享代码,此时就可以使用sbguard1.unlock()这样会更加灵活一些
如:
1 std::unique_lock<std::mutex> sbguard1(my_mutex1,std::defer_lock); //创建一个没有加锁的my_mutex1 2 sbguard1.lock(); //此时不用unlock(),但是加上也可以,目的是可以处理一些非共享代码 3 //...处理共享数据 4 sbguard1.unlock(); 5 6 //...处理非共享数据 7 8 //处理非共享数据完毕,再次lock()住 9 sbguard1.lock(); 10 11 //...处理共享数据 12 13 sbguard1.unlock(); //这句额可以不用,因为sbguard1.lock(); 会自动解锁 14 //lock()住的代码越少,代码执行效率越高
直接上使用方法,也是为了提高效率
1 std::unique_lock<std::mutex> sbguard1(my_mutex1,std::defer_lock); //创建一个没有加锁的my_mutex1,但是sbguard1和my_mutex1绑定了 2 if(sbguard1.try_lock() == true) //sbguard1.try_lock()表示给my_mutex1尝试加锁 3 { 4 //返回true表示拿到锁了 5 } 6 else 7 { 8 //返回false表示没有拿到锁,可以做一些其他的事情 9 }
std::unique_lock<std::mutex> sbguard1(my_mutex1,std::defer_lock);表示将unique_lock类对象sbguard1和my_mutex1绑定
而release就是解除这种绑定
使用方法:
1 std::unique_lock<std::mutex> sbguard1(my_mutex1); //表示将unique_lock类对象sbguard1和my_mutex1绑定,并且将my_mutex1锁住(lock()住) 2 std::mutex *ptx = sbguard1.release(); //意思是现在ptx接管了对my_mutex1互斥量的lock()和unlock()操作共享数据 3 4 ptx->unlock(); //此时需要使用ptx将my_mutex1解锁
6、unique_lock()所有权的传递
一个unique_lock对象只能和一个mutex对象绑定
但是unique_lock对象可以把mutex对象的所有权转移
但是以下转移方法是错误的:
1 std::unique_lock<std::mutex> sbguard1(my_mutex1); 2 std::unique_lock<std::mutex> sbguard2(sbguard1); //错误!!!
正确的转移方法:
1 std::unique_lock<std::mutex> sbguard1(my_mutex1); //sbguard1拥有my_mutex1的所有权 2 std::unique_lock<std::mutex> sbguard2(std::move(sbguard1)); //此时sbguard2拥有my_mutex1的所有权
转移方法二:
1 //创建一个返回值为unique_lock对象的函数 2 std::unique_lock<std::mutex> rtn_unique_lock() 3 { 4 std::unique_lock<std::mutex> tmpguard(my_mutex1); 5 return tempguard; //从函数返回一个局部的unique_lock对象是可以的,系统会调用unique_lock的移动构造函数,生成临时的tempguard对象 6 } 7 std::unique_lock<std::mutex> sbguard1=rtn_unique_lock(); //也相当于sbguard1与my_mutex1绑定
六、单例设计模式共享数据分析、解决、call_once
1、设计模式概述
设计模式的一些写法和平常代码写法不一样,别人不易看懂
是国外开发人员为大型开发项目而创建的,一般讲该项目划分为多个模块
2、单例设计模式---单例类
整个项目中,有某个或者某个特殊的类,只创建该类的一个类对象
单例类:只创建一个类对象的类
单例模式实现过程如下:
首先,将该类的构造函数私有化(目的是禁止其他程序创建该类的对象);
其次,在本类中自定义一个对象(既然禁止其他程序创建该类的对象,就要自己创建一个供程序使用,否则类就没法用,更不是单例);
最后,提供一个可访问类自定义对象的类成员方法(对外提供该对象的访问方式)。
直白的讲就是,你不能用该类在其他地方创建对象,而是通过该类自身提供的方法访问类中的那个自定义对象。
那么问题的关键来了,程序调用类中方法只有两种方式,①创建类的一个对象,用该对象去调用类中方法;②使用类名直接调用类中方法,格式“类名.方法名()”;
上面说了,构造函数私有化后第一种情况就不能用,只能使用第二种方法。
而使用类名直接调用类中方法,类中方法必须是静态的,而静态方法不能访问非静态成员变量,因此类自定义的实例变量也必须是静态的。
这就是单例模式唯一实例必须设置为静态的原因。
1 /* 2 单例类中的方法必须是静态方法的原因: 3 程序调用类中方法只有两种方式,①创建类的一个对象,用该对象去调用类中方法;②使用类名直接调用类中方法,格式“类名::方法名()”; 4 上面说了,构造函数私有化后第一种情况就不能用,只能使用第二种方法。 5 而使用类名直接调用类中方法,类中方法必须是静态的,而静态方法不能访问非静态成员变量,因此类自定义的实例变量也必须是静态的。 6 */ 7 8 9 #include <iostream> 10 11 using namespace std; 12 13 class MyClass //这是一个单例类 14 { 15 private: 16 MyClass() {}; //私有化构造函数,则在类外不可以通过MyClass m1;的方式类创建类对象m1 17 private: 18 static MyClass *m_instance; //静态成员变量 19 public: 20 static MyClass *GetInstance() 21 { 22 if (m_instance == NULL) //这个if保证每次返回的都是同一个类对象的指针 23 { 24 m_instance = new MyClass(); 25 static CGarhuishou c1; //因为c1为static类型,所以c1的生命周期为整个代码运行时间,当代码运行结束时,会自动调用c1的构造函数,也就delete掉了m_instance 26 } 27 return m_instance; 28 } 29 30 class CGarhuishou //类中套类,用来释放MyClass类对象 31 { 32 public: 33 ~CGarhuishou() 34 { 35 if (MyClass::m_instance) //如果m_instance被初始化了 36 { 37 delete m_instance; 38 MyClass::m_instance = NULL; 39 } 40 } 41 }; 42 void func() 43 { 44 cout << "测试" << endl; 45 } 46 47 }; 48 49 //类静态变量初始化 50 MyClass *MyClass::m_instance = NULL; //MyClass *是m_instance的类型说明符 51 52 int main() 53 { 54 MyClass *p_a = MyClass::GetInstance(); //创建一个类对象,GetInstance()返回的是一个MyClass类对象的地址 55 MyClass *p_b = MyClass::GetInstance(); //p_a和p_b会指向同一个类对象,这是由于GetInstance()中的if语句判断了是否已经创建了MyClass类对象 56 57 system("pause"); 58 return 0; 59 }
3、单例模式下解决不同线程访问共享数据或方法的问题
需要在我们自己创建的线程(而不是主线程)中来创建MyClass单例类对象
不能线程可能同时访问GetInstance(),并同时创建类对象m_instance,如果m_instance已经new过了,再次new就会出问题,此时需要对GetInstance()这种成员函数进行互斥的操作,方法如下:
1 #include <iostream> 2 #include <thread> 3 #include <mutex> 4 5 using namespace std; 6 7 std::mutex resource_mutex; //创建互斥量用来解决两个线程可能会同时执行m_instance = new MyClass();的问题 8 9 class MyClass //这是一个单例类 10 { 11 private: 12 MyClass() {}; //私有化构造函数,则在类外不可以通过MyClass m1;的方式类创建类对象m1 13 private: 14 static MyClass *m_instance; //静态成员变量 15 public: 16 static MyClass *GetInstance() 17 { 18 if (m_instance == NULL) //使用双重检查,如果m_instance被new(创建)过,那么就不会进入这个if循环,也就不会被锁住,以此提高效率 19 { 20 std::unique_lock<std::mutex> mymutex(resource_mutex); 21 //假设线程1和线程2都执行到这里的时候,再假设线程1先执行下去,由于线程1和线程2遇到的是一个互斥量,所以线程2会被阻塞住 22 //但是这样效率太低了,方法是用双重检查 23 if (m_instance == NULL) //这个if保证每次返回的都是同一个类对象的指针 24 { 25 m_instance = new MyClass(); 26 static CGarhuishou c1; //因为c1为static类型,所以c1的生命周期为整个代码运行时间,当代码运行结束时,会自动调用c1的构造函数,也就delete掉了m_instance 27 } 28 } 29 return m_instance; 30 } 31 32 class CGarhuishou //类中套类,用来释放MyClass类对象 33 { 34 public: 35 ~CGarhuishou() 36 { 37 if (MyClass::m_instance) //如果m_instance被初始化了 38 { 39 delete m_instance; 40 MyClass::m_instance = NULL; 41 } 42 } 43 }; 44 void func() 45 { 46 cout << "测试" << endl; 47 } 48 49 }; 50 51 //类静态变量初始化 52 MyClass *MyClass::m_instance = NULL; //MyClass *是m_instance的类型说明符 53 54 void mythread() 55 { 56 cout << "我的线程1开始执行" << endl; 57 MyClass *p_a = MyClass::GetInstance(); //在线程1中创建单例类对象p_a, 58 cout << "我的线程1执行完毕" << endl; 59 } 60 61 int main() 62 { 63 MyClass *p_a = MyClass::GetInstance(); //创建一个类对象,GetInstance()返回的是一个MyClass类对象的地址 64 MyClass *p_b = MyClass::GetInstance(); //p_a和p_b会指向同一个类对象,这是由于GetInstance()中的if语句判断了是否已经创建了MyClass类对象 65 66 p_b->func(); 67 68 std::thread myobj1(mythread); //创建线程1,在调用线程函数mythread()的时候会调用GetInstance() 69 std::thread myobj2(mythread); //创建线程2,线程1和线程2使用同一个入口函数mythread 70 myobj1.join(); //等待myobj1线程执行完毕 71 myobj2.join(); //等待myobj2线程执行完毕 72 //由于两个线程使用同一个入口函数,于是会与两个通路同时执行GetInstance() 73 //于是对于m_instance = new MyClass(); 这一句可能存在两个线程同时执行的情况,解决方法是使用互斥量 74 75 76 system("pause"); 77 return 0; 78 }
执行结果:
4、call_once()函数
功能:call_once(a)能够保证函数a()只被调用一次
解决单例模式下类对象创建代码可能被执行多次的问题
或者是解决单例模式下,多线程多次执行单例类对象多次初始化的问题
具备互斥量的能力,且比互斥量消耗的资源更少
std::once_flag是一个结构,用来标记a()是否执行
如果已经调用a()函数,那么std::once_flag设置为"已调用"状态
即解决下面例程中线程1和线程2都执行CreatInstance()函数的问题
1 #include <iostream> 2 #include <thread> 3 #include <mutex> 4 5 using namespace std; 6 7 std::mutex resource_mutex; //创建互斥量用来解决两个线程可能会同时执行m_instance = new MyClass();的问题 8 std::once_flag g_flag; //定义一个call_once(A)中A()是否成功执行的标识g_flag 9 10 class MyClass //这是一个单例类 11 { 12 private: 13 MyClass() {}; //私有化构造函数,则在类外不可以通过MyClass m1;的方式类创建类对象m1 14 private: 15 static MyClass *m_instance; //静态成员变量 16 public: 17 static void CreatInstance() //只被调用一次 18 { 19 std::chrono::milliseconds dura(20000); 20 std::this_thread::sleep_for(dura); //休息20s 21 22 cout << "CreatInstance()被执行了" << endl; 23 24 m_instance = new MyClass(); 25 static CGarhuishou c1; //因为c1为static类型,所以c1的生命周期为整个代码运行时间,当代码运行结束时,会自动调用c1的构造函数,也就delete掉了m_instance 26 } 27 static MyClass *GetInstance() 28 { 29 std::call_once(g_flag, CreatInstance); //加入两个线程都执行到了这里,其中线程1成功执行了CreatInstance(),那么线程了要等线程1执行完毕, 30 return m_instance; //但是此时g_flag标记CreatInstance()已被执行的状态,那么线程2就不会执行CreatInstance() 31 } 32 33 class CGarhuishou //类中套类,用来释放MyClass类对象 34 { 35 public: 36 ~CGarhuishou() 37 { 38 if (MyClass::m_instance) //如果m_instance被初始化了 39 { 40 delete m_instance; 41 MyClass::m_instance = NULL; 42 } 43 } 44 }; 45 void func() 46 { 47 cout << "测试" << endl; 48 } 49 50 }; 51 52 //类静态变量初始化 53 MyClass *MyClass::m_instance = NULL; //MyClass *是m_instance的类型说明符 54 55 void mythread() 56 { 57 cout << "我的线程1开始执行" << endl; 58 MyClass *p_a = MyClass::GetInstance(); //在线程1中创建单例类对象p_a, 59 cout << "我的线程1执行完毕" << endl; 60 } 61 62 int main() 63 { 64 MyClass *p_a = MyClass::GetInstance(); //创建一个类对象,GetInstance()返回的是一个MyClass类对象的地址 65 MyClass *p_b = MyClass::GetInstance(); //p_a和p_b会指向同一个类对象,这是由于GetInstance()中的if语句判断了是否已经创建了MyClass类对象 66 67 p_b->func(); 68 69 std::thread myobj1(mythread); //创建线程1,在调用线程函数mythread()的时候会调用GetInstance() 70 std::thread myobj2(mythread); //创建线程2,线程1和线程2使用同一个入口函数mythread 71 myobj1.join(); //等待myobj1线程执行完毕 72 myobj2.join(); //等待myobj2线程执行完毕 73 //由于两个线程使用同一个入口函数,于是会与两个通路同时执行GetInstance() 74 //于是对于m_instance = new MyClass(); 这一句可能存在两个线程同时执行的情况,解决方法是使用互斥量 75 76 77 system("pause"); 78 return 0; 79 } 80 81 //假设线程1速度比线程2速度快,那么线程1会先执行mythread(),然后执行GetInstance(),然后执行CreatInstance(),然乎线程1开始睡觉 82 //在线程1开始睡觉的时候,线程2也是先执行mythread(),然后执行GetInstance(),但是线程2执行到CreatInstance()中的 83 //std::call_once(g_flag, CreatInstance)会卡在那里,等到线程1执行完CreatInstance()的时候,线程2就不会再执行std::call_once(g_flag, CreatInstance) 84 //即线程2不会再执行CreatInstance()函数 85 86 //最后最好在主线程中创建单例对象,然后在子线程中使用该单例对象
假设线程1速度比线程2速度快,那么线程1会先执行mythread(),然后执行GetInstance(),然后执行CreatInstance(),然乎线程1开始睡觉
在线程1开始睡觉的时候,线程2也是先执行mythread(),然后执行GetInstance(),但是线程2执行到CreatInstance()中的
std::call_once(g_flag, CreatInstance)会卡在那里,等到线程1执行完CreatInstance()的时候,线程2就不会再执行std::call_once(g_flag, CreatInstance)
即线程2不会再执行CreatInstance()函数
最后最好在主线程中创建单例对象,然后在子线程中使用该单例对象
七、条件变量 std::condition_varianle、wait()、notify_one()
1、提高效率的方法--
1 if (!msgRecvQueue.empty()) //如果msgRecvQueue为空,则不执行unique_lock(),也就避免了出现阻塞的情况 2 { 3 std::unique_lock<std::mutex> sbguard1(my_mutex1); 4 std::unique_lock<std::mutex> sbguard2(my_mutex2); 5 if (!msgRecvQueue.empty()) 6 { 7 //消息不为空 8 command = msgRecvQueue.front(); 9 msgRecvQueue.pop_front(); //移除取出的元素 10 //以下可以考虑处理数据... 11 return true; 12 } 13 } 14 else 15 { 16 std::chrono::millionseconds dura(20000); //如果线程为空,则睡20s,让其他线程执行 17 std::this_thread::sleep_for(dura); 18 }
(2)std::condition_variable类和该类中的wait()、notify_one()方法
std::condition_variable是一个和条件相关的类类,要和互斥量配合工作
一般是作为类中的私有变量
1 std::condition_variable my_cond; //生成一个条件对象 2 my_cond.wait(参数1,参数2);
wait()是std::condition_variable类下的一个成员函数,其中参数2是一个lambda表达式;
a) 如果第二个参数lambda表达式返回值是true,那么wait()将直接返回,不会阻塞,继续向下执行;
b) 如果第二个参数lambda表达式返回值是false,那么wait()将解锁互斥量my_mutex1,并阻塞到本行,阻塞到其他某个线程调用notifuy_one()成员函数为止。当其他线程调 用notify_one()函数则会唤醒out线程的wait()函数(原来wait()函数第二个参数返回值为false,wait()处于睡眠/阻塞状态),wait()就不断的尝试给原来的互斥量加锁,如果获 取不到,那么就会卡在wait()这里等着获取该互斥量加锁如果获取到,那么wait()就走下来了。
c) 如果wait()函数没有第二个参数,即my_cond.wait(sbguard1),那么第二个参数(参数2lambda表达式的默认返回值为false),那么wait()将解锁互斥量,并阻塞到本行, 阻塞到其他某个线程执行my_cond.notifuy_one()成员函数为止。当其他线程调用notify_one()函数则会唤醒out线程的wait()函数(原来wait()函数第二个参数返回值为 false,wait()处于睡眠/阻塞状态),wait()就不断的尝试给原来的互斥量加锁,如果获取不到,那么就会卡在wait()这里等着获取该互斥量加锁如果获取到,那么wait()就走 下来了。
如果wait()函数加参数2,且被其他线程的notify_one()唤醒之后,wait()的第二个参数返回值还是为false,此时依旧会对互斥量解锁,并阻塞在wait()这里。
如果wait()不加参数2,那么wait()被其他线程的notify_one()唤醒之后,会无条件的继续向下执行
代码如下:
1 //使用condition_variable类和该类对应的成员函数wait()notify_one()解决死锁问题 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 for (int i = 0; i < 100000; i++) 16 { 17 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 18 std::unique_lock<std::mutex> sbguard1(my_mutex1); //给互斥量my_mutex1加锁,且不用手动解锁 19 std::unique_lock<std::mutex> sbguard2(my_mutex2); //线程1执行到这里的时候,如果线程2已经给互斥量my_mutex2加锁,那么线程1就会阻塞在这里 20 //中间可能隔了很多代码(需要保护不同的数据块) 21 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 22 23 my_cond.notify_one(); //如果out线程中的wait()处于阻塞的状态,那么这一句将会将out线程的wait()唤醒 24 } 25 } 26 //把数据从消息队列中取出的线程 27 void outMsgRecvQueue() 28 { 29 int command = 0; 30 while (true) 31 { 32 std::unique_lock<std::mutex> sbguard1(my_mutex1); 33 //wait()是std::condition_variable类下的一个成员函数 34 //如果第二个参数lambda表达式返回值是true,那么wait()将直接返回,不会阻塞,继续向下执行 35 //如果第二个参数lambda表达式返回值是false,那么wait()将解锁互斥量my_mutex1,并阻塞到本行 36 //阻塞到其他某个线程调用notifuy_one()成员函数为止 37 //如果wait()函数没有第二个参数,即my_cond.wait(sbguard1),那么第二个参数(lambda表达式的默认返回值为false)么wait()将解锁互斥量,并阻塞到本行 38 //阻塞到其他某个线程调用notifuy_one()成员函数为止(第二个参数返回值为flase的情况) 39 // 40 my_cond.wait(sbguard1, [this]{ //一个lambda表达式就是一个可调用对象(函数) 41 if (!msgRecvQueue.empty()) 42 return true; 43 else 44 return false; 45 }); 46 command = msgRecvQueue.front(); 47 msgRecvQueue.pop_front(); //移除取出的元素 48 sbguard1.unlock(); //unique_lock()可以使用类对象随时unlock,但是unique_lock也可以自动的unlock 49 cout << "outMsgRecvQueue,取出一个元素:" << command << endl; 50 } 51 } 52 53 private: 54 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 55 std::mutex my_mutex1; //创建一个互斥量1 56 std::mutex my_mutex2; //创建一个互斥量2 57 std::condition_variable my_cond; //生成一个条件对象 58 }; 59 int main() 60 { 61 A myobja; //生成类对象 62 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 63 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 64 myOutMsgObj.join(); 65 myInMsgObj.join(); 66 67 system("pause"); 68 return 0; 69 } 70 71 /* 72 一般在主函数中,先创建那个线程,则哪个线程对应的函数先执行,那么在本函数中,outMsgRecvQueue()线程先创建,那么outMsgRecvQueue()先执行 73 但是也存在inMsgRecvQueue()先执行的情况 74 那么该代码的执行流程为: 75 1)执行outMsgRecvQueue()线程,由于该线程是先执行的,所以会先利用unique_lock()给my_mutex1加锁,然后执行wait(),但是此时还没 76 有执行inMsgRecvQueue()线程,所以msgRecvQueue消息队列为空,则wait()第二个参数返回值为false,outMsgRecvQueue()线程将会阻塞 77 在wait()这里,并将my_mutex1解锁。 78 2)系统执行inMsgRecvQueue()线程,由于my_mutex1互斥量是出于解锁状态,所以inMsgRecvQueue()线程会给my_mutex1加锁,然后给 79 msgRecvQueue添加数字进去,并执行my_cond.notify_one(),执行到inMsgRecvQueue()线程最后,给my_mutex1解锁; 80 3)由于inMsgRecvQueue()线程调用了notify_one()成员函数,所以outMsgRecvQueue()线程中的wait()将会被唤醒,并给my_mutex1加锁(如果加锁不成功,将会阻塞) 81 由于此时my_mutex1是处于解锁状态,所以wait()会给my_mutex1加锁成功,且此时msgRecvQueue中是有消息的了,wait()第二个参数返回值为true,outMsgRecvQueue()线程 82 将会跳过wait(),继续执行接下来的代码。 83 */
/* 一般在主函数中,先创建那个线程,则哪个线程对应的函数先执行,那么在本函数中,outMsgRecvQueue()线程先创建,那么outMsgRecvQueue()先执行 但是也存在inMsgRecvQueue()先执行的情况 那么该代码的执行流程为: 1)执行outMsgRecvQueue()线程,由于该线程是先执行的,所以会先利用unique_lock()给my_mutex1加锁,然后执行wait(),但是此时还没 有执行inMsgRecvQueue()线程,所以msgRecvQueue消息队列为空,则wait()第二个参数返回值为false,outMsgRecvQueue()线程将会阻塞 在wait()这里,并将my_mutex1解锁。 2)系统执行inMsgRecvQueue()线程,由于my_mutex1互斥量是出于解锁状态,所以inMsgRecvQueue()线程会给my_mutex1加锁,然后给 msgRecvQueue添加数字进去,并执行my_cond.notify_one(),执行到inMsgRecvQueue()线程最后,给my_mutex1解锁; 3)由于inMsgRecvQueue()线程调用了notify_one()成员函数,所以outMsgRecvQueue()线程中的wait()将会被唤醒,并给my_mutex1加锁(如果加锁不成功,将会阻塞) 由于此时my_mutex1是处于解锁状态,所以wait()会给my_mutex1加锁成功,且此时msgRecvQueue中是有消息的了,wait()第二个参数返回值为true,outMsgRecvQueue()线程 将会跳过wait(),继续执行接下来的代码。 */
执行结果:
上述程序不好的地方:
(1)可能in线程执行notify_one()唤醒out线程后,然后又执行了in线程中的unique_lock()并对in线程中的互斥量加锁,导致out线程中的wait()不能重新
给out线程中的互斥量加锁,结果就是in线程执行了好多次,out线程也执行不了一次。
(2)假如out线程并没有卡在wait()线程那里,而是去执行别的地方了代码了,那么in线程中执行notify_one()就对线程2中的wait()没有效果
类似于你在家睡觉,家长能够叫醒你,但是你不在家睡觉,那么家长在家叫你是叫不醒的。
上述代码的深入思考
(1)in线程执行的次数较多,但是out线程执行的次数较少,那么需要对in线程进行限流
(2)原来程序中out线程是先执行in线程后先执行的,那么此时out线程会卡在wait()那里,in线程中的notify_one()会起作用
但是如果in线程先创建,那么in线程中的notify_one()先执行的话,out线程中的wait()就不会被唤醒,即in线程中的notify_one()没有起到作用
(3)std::condition_variable类和该类中的wait()、notify_all()方法
问题的提出:
in线程中的notify_one()只能唤醒一个线程中的wait(),加入有多个线程中有wait(),但是只有一个线程中有nitify_one(),那么唤醒哪个线程中的wait()是不确定的。但是每个线程中的wait()都有机会被唤醒。
假如有两个out线程,现在两个out线程中的wait()都需要被唤醒,而in线程中只有一个notify_one(),如果想让in线程唤醒out线程1和out线程2中的wait(),可以在in线程中使用notify_all()。
例程:
1 //使用condition_variable类和该类对应的成员函数wait()notify_all()解决死锁问题 2 #include <iostream> 3 #include <thread> //尖括号表示系统头文件 4 #include <list> 5 #include <mutex> //lock() 6 7 using namespace std; 8 9 class A 10 { 11 public: 12 //把收到的信息(玩家命令)放入到一个队列的线程 13 void inMsgRecvQueue() 14 { 15 for (int i = 0; i < 100000; i++) 16 { 17 cout << "inMsgRecvQueue执行,插入一个数据:" << i << endl; 18 std::unique_lock<std::mutex> sbguard1(my_mutex1); //给互斥量my_mutex1加锁,且不用手动解锁 19 std::unique_lock<std::mutex> sbguard2(my_mutex2); //线程1执行到这里的时候,如果线程2已经给互斥量my_mutex2加锁,那么线程1就会阻塞在这里 20 //中间可能隔了很多代码(需要保护不同的数据块) 21 msgRecvQueue.push_back(i); //假设数字i就是收到的玩家命令 22 23 my_cond.notify_all(); //如果out线程中的wait()处于阻塞的状态,那么这一句将会将out线程的wait()唤醒 24 } 25 } 26 //把数据从消息队列中取出的线程 27 void outMsgRecvQueue() 28 { 29 int command = 0; 30 while (true) 31 { 32 std::unique_lock<std::mutex> sbguard1(my_mutex1); 33 //wait()是std::condition_variable类下的一个成员函数 34 //如果第二个参数lambda表达式返回值是true,那么wait()将直接返回,不会阻塞,继续向下执行 35 //如果第二个参数lambda表达式返回值是false,那么wait()将解锁互斥量my_mutex1,并阻塞到本行 36 //阻塞到其他某个线程调用notifuy_one()成员函数为止 37 //如果wait()函数没有第二个参数,即my_cond.wait(sbguard1),那么第二个参数(lambda表达式的默认返回值为false)么wait()将解锁互斥量,并阻塞到本行 38 //阻塞到其他某个线程调用notifuy_one()成员函数为止(第二个参数返回值为flase的情况) 39 // 40 my_cond.wait(sbguard1, [this]{ //一个lambda表达式就是一个可调用对象(函数) 41 if (!msgRecvQueue.empty()) 42 return true; 43 else 44 return false; 45 }); 46 command = msgRecvQueue.front(); 47 msgRecvQueue.pop_front(); //移除取出的元素 48 cout << "outMsgRecvQueue,取出一个元素:" << command << ",当前线程id=" << std::this_thread::get_id() << endl; 49 sbguard1.unlock(); //unique_lock()可以使用类对象随时unlock,但是unique_lock也可以自动的unlock 50 } 51 } 52 53 private: 54 std::list<int> msgRecvQueue; //list容器,专门用来收取玩家发来的命令,即共享数据 55 std::mutex my_mutex1; //创建一个互斥量1 56 std::mutex my_mutex2; //创建一个互斥量2 57 std::condition_variable my_cond; //生成一个条件对象 58 }; 59 int main() 60 { 61 A myobja; //生成类对象 62 std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja); //第二个参数是引用,才能保证主线程中的myobja和线程myOutMsgObj中的myobja是同一个类对象 63 std::thread myOutMsgObj2(&A::outMsgRecvQueue, &myobja); //线程myOutMsgObj和线程myOutMsgObj2都执行一个函数outMsgRecvQueue() 64 std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja); 65 myOutMsgObj.join(); 66 myOutMsgObj2.join(); 67 myInMsgObj.join(); 68 //假如有两个out线程,现在两个out线程中的wait()都需要被唤醒,而in线程中只有一个notify_one(),那么需要对in线程中的notify_one()做更改 69 70 system("pause"); 71 return 0; 72 }