互斥量概念、用法、死锁演示及解决详解
保护共享数据,操作时用代码把共享数据锁住,其他像操作共享数据的线程必须等待,等待解锁,锁住,操作,解锁。
(1)互斥量(mutex)的基本概念
互斥量:类对象,理解成一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功(成功的标志是Lock()函数返回),如果没锁成功,那么流程将卡在lock()这里不断的尝试去锁。
互斥量使用要小心,保护数据不能多也不能少,少了,没达到保护效果,多了,影响效率。
(2)互斥量的用法
(2.1)lock(),unlock()
步骤:先lock()加锁,操作共享数据,unlock()
lock()和unlock()要成对使用,有一次lock()必然要有一次unlock(),非对称数量的调用,必然会导致程序的崩溃
if(true)对应一个解锁,if(false)对应一个解锁
#include <iostream>
#include<thread>
#include<vector>
#include<list>
#include<mutex>
using namespace std;
class A {
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
my_mutex.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列中来;
my_mutex.unlock();
}
return;
}
bool outMsgLULProc(int &command)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front();//移除第一个元素,但不返回;
my_mutex.unlock();
return true;
}//if(true)对应一个解锁,if(false)对应一个解锁
my_mutex.unlock();
return false;
}
//把数据从消息队列中取出的线程:
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 10000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑数据的处理了……
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int>msgRecvQueue;//容器(消息队列),专门用于代表玩家给咱们发送过来的命令
std::mutex my_mutex;//创建了一个互斥量
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, ref(myobja));//第二个参数是引用,才能保证线程里用的是用一个对象
thread myInMsgObj(&A::inMsgRecvQueue, ref(myobja));
myOutMsgObj.join();
myInMsgObj.join();
//步骤:先lock()加锁,操作共享数据,unlock()
return 0;
}
有lock,忘记unlock的问题,非常难排查,开发时要格外注意
为了防止出现这个问题,引入lock_guard()的类模板
(2.2)std::lock_guard()
功能:忘了unlock()不要紧,lock_guard()自动调用unlock()
智能指针(unique_ptr<>):忘了释放内存不要紧,智能指针帮忙释放;
lock_guard()可以直接取代lock()、unlock(),用了lock_guard()之后,就不能再调用lock()、unlock();
lock_guard()构造函数里执行了mutex::lock()函数;
析构函数里执行了mutex::unlock()函数。
#include <iostream>
#include<thread>
#include<vector>
#include<list>
#include<mutex>
using namespace std;
class A {
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
my_mutex.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列中来;
my_mutex.unlock();
}
return;
}
bool outMsgLULProc(int &command)
{
std::lock_guard<std::mutex>sbguard(my_mutex);//sbguard时随便起的对象名
//my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front();//移除第一个元素,但不返回;
//my_mutex.unlock();
return true;
}//if(true)对应一个解锁,if(false)对应一个解锁
//my_mutex.unlock();
return false;
}
//把数据从消息队列中取出的线程:
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 10000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑数据的处理了……
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int>msgRecvQueue;//容器(消息队列),专门用于代表玩家给咱们发送过来的命令
std::mutex my_mutex;//创建了一个互斥量
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, ref(myobja));//第二个参数是引用,才能保证线程里用的是用一个对象
thread myInMsgObj(&A::inMsgRecvQueue, ref(myobja));
myOutMsgObj.join();
myInMsgObj.join();
//步骤:先lock()加锁,操作共享数据,unlock()
return 0;
}
(3)死锁
张三:站在北京等李四,不挪窝;
李四:站在深圳等张三,不挪窝。
C++中,假设有两把锁(死锁产生的前提条件是有至少两把锁头,即两个互斥量)金锁(JinLock)、银锁(YinLock)
假设有两个线程A,B。
- (1)线程A执行的时候,这个线程先锁金锁,把金锁Lock()成功了,然后他去尝试lock银锁
- (2)上下文切换(切线程)
- (3)线程B执行了,这个线程先锁银锁,因为银锁还没有被锁,所以银锁会被lock成功,然后线程B要去lock金锁
- (4)死锁现象产生
线程A由于拿不到银锁,流程走不下去(虽然后边代码有解锁金锁的但是流程走不下去,所以金锁解不开)
线程B由于拿不到金锁,流程走不下去(虽然后边代码有解锁银锁的但是流程走不下去,所以银锁解不开)
(3.1)死锁演示
#include <iostream>
#include<thread>
#include<vector>
#include<list>
#include<mutex>
using namespace std;
class A {
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
my_mutex1.lock();//实际工程中这两个锁头代码不一定挨着,可能她们需要保护不同的数据共享块
my_mutex2.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列中来;
my_mutex2.unlock();
my_mutex1.unlock();
}
return;
}
bool outMsgLULProc(int &command)
{
//std::lock_guard<std::mutex>sbguard(my_mutex1);//sbguard时随便起的对象名
my_mutex2.lock();
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front();//移除第一个元素,但不返回;
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}//if(true)对应一个解锁,if(false)对应一个解锁
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//把数据从消息队列中取出的线程:
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 10000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑数据的处理了……
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int>msgRecvQueue;//容器(消息队列),专门用于代表玩家给咱们发送过来的命令
std::mutex my_mutex1;//创建了一个互斥量
std::mutex my_mutex2;//创建了另外一个互斥量
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, ref(myobja));//第二个参数是引用,才能保证线程里用的是用一个对象
thread myInMsgObj(&A::inMsgRecvQueue, ref(myobja));
myOutMsgObj.join();
myInMsgObj.join();
//步骤:先lock()加锁,操作共享数据,unlock()
return 0;
}
出现死锁:
(3.2)死锁的一般解决方案
只要保证这两个互斥量上锁的顺序一致就不会造成死锁。
调换两个互斥量上锁顺序后,死锁解决:
(3.3)std::lock()函数模板
能力:一次同时锁住两个或者是两个以上的互斥量(至少两个,多了不限),它不存在这种因为在多个线程中因为上锁顺序问题导致的死锁风险的问题
std::lock():如果互斥量中有一个没锁住,它就在那等着,等所有互斥量都锁住,他才能往下走;
要么两个互斥量同时锁住,要么两个互斥量都没锁住。一旦有一个没锁住,std::lock()将立即把已经锁住的互斥量释放(解锁),等待两个同时可以锁住的时机;
用来处理多个互斥量的时候
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
//my_mutex1.lock();//实际工程中这两个锁头代码不一定挨着,可能她们需要保护不同的数据共享块
//my_mutex2.lock();
std::lock(my_mutex1, my_mutex2);//相当于每个互斥量都调用了lock()
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列中来;
my_mutex2.unlock();
my_mutex1.unlock();
}
(3.4)std::lock_guard的std:adopt_lock参数
std::adopt_lock参数是个结构体对象,起一个标记作用,作用就是表示这个互斥量已经lock()了,不需要在std::lock_guardstd::mutexsbguard1中了
总结:std::lock()一次锁多个互斥量;谨慎使用(建议一个一个的锁)
void inMsgRecvQueue()
{
for (int i = 0; i < 10000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
//my_mutex1.lock();//实际工程中这两个锁头代码不一定挨着,可能她们需要保护不同的数据共享块
//my_mutex2.lock();
std::lock(my_mutex1, my_mutex2);//相当于每个互斥量都调用了lock()
std::lock_guard<std::mutex>sbguard1(my_mutex1,std::adopt_lock);//sbguard1时随便起的对象名
std::lock_guard<std::mutex>sbguard2(my_mutex2,std::adopt_lock);//sbguard2时随便起的对象名
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接弄到消息队列中来;
/* my_mutex2.unlock();
my_mutex1.unlock();*/
}
return;