#include <list>
#include <thread>
class A
{
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0;i < 100000;++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
msgRecvQueue.push_back(i);
}
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
for (int i = 0;i < 100;i++)
{
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
//这里考虑处理数据
}
else
{
cout << "outMsgRecvQueue()执行,但是目前消息队列中为空" << endl;
}
}
}
private:
std::list<int> msgRecvQueue; //容器,专门用于代表玩家给咱们发送过来的命令
};
int main()
{
A myobja;
std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutnMsgObj.join();
return 0;
}
保护共享数据,操作时某个线程用代码把共享数据锁住,操作数据、解锁;
其他想操作共享数据的线程必须等待解锁,锁定住,操作,解锁。
一、互斥量的概念
互斥量(mutex)的基本概念,多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标志是lock()函数返回)
如果没锁成功,那么流程卡在lock()这里不断尝试去锁这把锁头
二、互斥量的用法
#include <mutex>
2.1 lock() unlock()
步骤:先lock(),操作共享数据,再unlock(),每调用一次lock(),必然应该调用一个unlock()
#include <list>
#include <thread>
#include <mutex>
class A
{
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0;i < 100;++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
my_mutex.lock();
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
}
bool outMsgLULProc(int& command)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex.unlock();
return true;
}
my_mutex.unlock();
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0;i < 100;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;
std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutnMsgObj.join();
return 0;
}
为了防止大家忘记unlock(),引入了一个叫std::lock_guard()的类模板:你忘记unlock()不要紧,我替你unlock()。
学习过智能指针(unique_ptr):你忘记释放内存不要紧,我替你释放。
2.1 std::lock_guard类模板,直接取代lock()和unlock(),也就是说,用了lock_guard以后,再不能使用lock()和unlock()
记住这种写法:
std::lock_guard<std::mutex> sguard(my_mutex);
其中,my_mutex的定义在:
std::mutex my_mutex1; //创建一个互斥量
生成std::lock_guard<std::mutex>
类模板的对象肯定要调用类的构造函数,构造函数里执行了mutex::lock()
sguard
是局部对象,局部对象退出函数(return的时候)需要析构,析构函数调用了mutex::unlock()
bool outMsgLULProc(int& command)
{
// my_mutex.lock();
std::lock_guard<std::mutex> sguard(my_mutex);
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
// my_mutex.unlock();
return true;
}
// my_mutex.unlock();
return false;
}
lock_guard不够灵活:
lock()、unlock()随时能调用,随时能解锁
lock_guard只能在析构的时候调用解锁
通过大括号可以使得只要到大括号末尾lock_guard
就会析构掉
原则:在互斥量包裹的位置之外,不要动共享数据
三、死锁
张三:站在北京等李四,不挪窝;
李四:站在北京等张三,不挪窝;
C++中:
比如有两把锁(死锁这个问题,至少有两个锁头 两个互斥量才能产生)
两个线程A和B
(1)线程A执行的时候,这个线程先锁金锁,把金锁lock()成功了,然后去lock银锁;
出现了上下文切换(此时银锁还没有锁就出现了上下文切换)
个人理解:关键在于两个lock之间发生了上下文切换
(2)线程B执行了,这个线程先锁银锁,因为银锁还没有被锁,所以银锁会lock成功,线程B要去lock金锁
此时此刻,死锁就产生了;
(3)线程A因为拿不到银锁头,流程走不下去(后边代码有解锁金锁头但是流程走不下去,所以金锁头解不开)
(4)线程B因为拿不到金锁头,流程走不下去(后边代码有解锁银锁头但是流程走不下去,所以银锁头解不开)
3.1 死锁演示
#include <list>
#include <thread>
#include <mutex>
class A
{
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0;i < 100000;++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
my_mutex1.lock();
my_mutex2.lock();
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
}
}
bool outMsgLULProc(int& command)
{
my_mutex2.lock();
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0;i < 100000;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;
std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
myInMsgObj.join();
myOutnMsgObj.join();
return 0;
}
3.2 死锁的一般解决方案
只要保证两个互斥量上锁的顺序保持一致就不会死锁。
3.3 std::lock()函数模板
用来处理多个互斥量的时候才出场
能力:一次锁住两个或者两个以上互斥量(至少两个,多了不限,1个不行)
它不存在这种因为在多个线程中,因为锁的问题导致死锁的风险问题;
std::lock():如果互斥量中有一个没锁柱,他就在那里等着,等着所有互斥量都锁住,他才能往下走(返回)
特点:要么两个互斥量都锁住,要么两个互斥量都没锁住 。
要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。
std::lock(my_mutex1,my_mutex2);
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
std::lock 如果遇到一个锁住,一个没锁住,就会把没锁住的那个撒开
3.4 std::lock_guard的std::adopt_lock参数
std::lock(my_mutex1,my_mutex2);
std::lock_guard<std::mutex> sguard1(my_mutex1,std::adopt_lock);
std::lock_guard<std::mutex> sguard2(my_mutex2,std::adopt_lock);
msgRecvQueue.push_back(i);
// my_mutex2.unlock();
// my_mutex1.unlock();
曾经讲过
std::lock_guard<std::mutex> sguard1(my_mutex1);
该行代码的含义是:
在该对象的构造函数中会调用互斥量my_mutex1的std::mutex::lock()
函数,在析构函数中会调用互斥量my_mutex1的std::mutex::unlock()
函数。
而现在的情况是:
std::lock(my_mutex1,my_mutex2);
相当于把两个互斥量的std::mutex::lock()
函数调用完了,就不希望std::lock_guard<std::mutex> sguard1(my_mutex1);
对象的构造函数中再调用std::mutex::lock()
函数
而该对象析构时照常,调用my_mutex1和my_mutex2的unlock()函数
std::adopt_lock()
是个结构体对象,起一个标记作用。
作用就表示这个互斥量已经lock(),不需要在std::lock_guard<std::mutex>
的对象的构造函数里对mutex类对象再lock()了。
总结:
std::lock()一次锁定多个互斥量,谨慎使用(建议一个一个锁)