一、互斥量mutex
保护共享数据,操作时,用代码把共享数据锁住,操作数据,解锁。其他线程要操作共享数据的线程必须等待解锁,锁住,操作,解锁。
互斥量就是类对象,一个锁,多个线程用lock()成员函数加锁这个锁头,只有一个线程能锁成功,成功的标志是lock函数返回,如果没有锁成功,那么流程就卡在lock()这里,不断的尝试去锁这个锁头。
互斥量使用要小心,保护数据多了,影响效率,保护少了,没达到保护效果。
二、互斥量用法
1、lock()、unlock()
include <mutex>
先lock,操作共享数据,再unlock。
使用规则:成对使用,有lock必须有unlock;
1 #include <iostream> 2 #include <thread> //线程 3 #include <vector> 4 #include <list> 5 #include <mutex> 6 /* 7 网络游戏服务器 8 创建两个线程,一个线程收集玩家命令(用一个数字代表玩家命令),并把命令数据写到一个队列中; 9 另一个线程从队列中取出玩家发送来的命令,解析,然后执行玩家需要的动作。 10 list:频繁顺序插入和删除数据时效率高; 11 vector:频繁随机插入和删除数据时效率高; 12 准备使用成员函数来作为线程函数 13 */ 14 using namespace std; 15 class A{ 16 public: 17 //把收到的消息(玩家命令)让入到一个队列中的线程函数 18 void InMsgQue(){ 19 for(int i=0;i<100;i++){ 20 cout<<"InMsgQue执行,插入一个元素"<<i<<endl; 21 my_mutex.lock(); 22 MyQue.push_back(i);//假设i就是命令 23 my_mutex.unlock(); 24 } 25 26 } 27 bool outMsgLULProc(int &command){//这样的目的是为了方便后序对command的操作 28 my_mutex.lock(); 29 if(!MyQue.empty()){ 30 int command = MyQue.front(); 31 MyQue.pop_front(); 32 my_mutex.unlock();//这个unlock特别容易被忘记写 33 return true; 34 } 35 my_mutex.unlock(); 36 return false; 37 } 38 //把数据从消息队列中取出来的线程函数 39 void OutMsgQue(){ 40 int command=0; 41 for(int i=0;i<100;i++){ 42 bool result = outMsgLULProc(command); 43 if(result){ 44 //消息不空 45 cout<<"OutMsgQue正在执行,取出一个元素"<<command<<endl; 46 //接下来就考虑处理这个取出来的数据command......... 47 } 48 else{ 49 cout << "OutMsgQue执行,但是消息队列为空"<<i << endl; 50 } 51 52 } 53 cout<<"end"<<endl; 54 } 55 56 private: 57 list<int> MyQue; 58 std::mutex my_mutex;//创建一个互斥量 59 }; 60 61 int main(){ 62 A myobj; 63 std::thread myout(&A::OutMsgQue,&myobj); 64 std::thread myin(&A::InMsgQue,&myobj);//必须市引用,才能保证线程用的是同一个对象 65 myout.join(); 66 myin.join(); 67 cout<<"main end"<<endl; 68 return 0; 69 }
2、std::lock_guard类模板
为了防止忘记unlock,引入lock_guard,类似于智能指针,他会帮你unlock。
1 bool outMsgLULProc(int &command){ 2 std::lock_guard<std::mutex> guardd(my_mutex); 3 if(!MyQue.empty()){ 4 int command = MyQue.front(); 5 MyQue.pop_front(); 6 return true; 7 } 8 return false; 9 }
lock_guard构造函数执行了lock;析构函数执行了unlock。
只有return的时候才unlock,没有分开使用lock和unlock灵活。
可以用大括号提前结束guard:
1 void InMsgQue(){ 2 for(int i=0;i<100;i++){ 3 cout<<"InMsgQue执行,插入一个元素"<<i<<endl; 4 { 5 std::lock_guard<std::mutex> sbguard(my_mutex); 6 MyQue.push_back(i);//假设i就是命令 7 }//大括号结束,lock_guard析构函数执行 8 //其他处理代码、、、、、、、 9 } 10 11 }
三、死锁
1、演示
举例:张三在北京等李四,不挪窝;李四在深圳等张三,不挪窝。
c++中:
死锁问题是有至少两个锁头也就是两个互斥量才会产生。
有两个锁:金锁,银锁
线程:A 和 B
线程A先锁金锁,金锁锁成功,然后去lock银锁。。。
线程B先锁银锁,银锁还没被锁的话,锁银锁成功了,然后去lock金锁。。。但是金锁被A锁了。。。
A拿不到银锁,金锁解不开,B去拿金锁,但是金锁被A锁了,所以银锁解不开。
这时就发生死锁了。AB两个线程就干等着直到永远~~
2、解决方案
只要保证两个互斥量的lock顺序一致,就不会发生死锁。
3、std::lock()函数模板
用来处理多个互斥量。
可以一次锁住两个或多个互斥量。
它不存在由于多个线程中因为锁的顺序问题导致的死锁。
如果一个锁没锁住,他就会马上解锁之前所有锁上的互斥量,然后在那儿等着,等所有的互斥量都能够锁住,它才能往下走。
要么都锁住,要么都没有锁住。
1 void InMsgQue(){ 2 for(int i=0;i<100;i++){ 3 cout<<"InMsgQue执行,插入一个元素"<<i<<endl; 4 std::lock(my_mutex1,my_mutex2);//相当于两个互斥量都调用了lock 5 MyQue.push_back(i);//假设i就是命令 6 my_mutex1.unlock(); 7 my_mutex2.unlock(); 8 //其他处理代码、、、、、、、 9 } 10 11 }
容易忘记unlock
4、std::lock_guard的std::adopt_lock参数
使用lock模板,还是容易忘记unlock,那么可是使用lock_guard的adopt_lock参数:
1 void InMsgQue(){ 2 for(int i=0;i<100;i++){ 3 cout<<"InMsgQue执行,插入一个元素"<<i<<endl;
std::lock(my_mutex1,my_mutex2); 4 std::lock_guard<std::mutex> sbguard(my_mutex1,std::adopt_lock); 5 std::lock_guard<std::mutex> sbguard2(my_mutex2,std::adopt_lock); 6 MyQue.push_back(i);//假设i就是命令 7 //其他处理代码、、、、、、、 8 } 9 }
这个参数就是起一个标记作用,就是如果互斥量已经被lock了,那就不会再调用lock_guard的构造函数了,就只剩析构函数了