保护共享数据,操作时,某个线程用代码把共享数据锁住、操作数据、解锁。那么其他想操作共享数据的线程就必须等待锁住的线程解锁才能对共享数据进行操作。
《1》互斥量(mutex)的基本概念(互斥量是整个多线程开发中最重要最核心的知识点!)
互斥量(mutex):互斥量是个类对象。我们可以理解为一把锁,多个线程都尝试用lock()成员函数来加锁这把锁头,但是只有一个线程会成功锁住(成功的标志是lock()函数成功返回)。如果某个线程卡在lock()这里,那么程序就会不断地尝试去锁住这把锁头(因为锁不上呀,所以就会一直卡在这儿等着别的lock()被unlock()后,这把锁才能lock()上);
格式:
std::mutex mutexObjName;//创建一个名为mutexObjName的互斥量
解释:要想正确地使用互斥量来保护我们想保护的数据,没有那么简单。保护少了就没有达到保护的效果,保护多了(你搁这儿锁着,其他人想用这段共享的内存数据都用不了,这样效率肯定低!),又会导致你的多线程代码效率低下,这需要你大量地编写多线程的代码。这样才能写出好的多线程代码。
《2》互斥量的用法
使用互斥量之前,必须包含头文件:
#include
在Vs/Vscode下你都可以直接包含这个头文件即可使用该mutex类。
2.1》lock(),unlock()
lock():锁住线程
unlock():解锁线程
每个线程对于共享数据使用mutex的步骤:1lock,2操作共享数据,(操作完毕后)3unlock()。
注意:lock()和unlock()必须要成对地谨慎地使用。(这样才不会出问题)
解释:类似调用1次lock(),却调用2次unlock()或者调用2次lock(),却调用1次unlock()。这里不是说,一定不能这么干,你lock住的函数退出时,必须要补上unlock,如果你有2条return 语句,那你有2个unlock就是非常合理的!
2.2》std::lock_guard类模板
由于使用lock()和unlock()时,非常容易导致写了lock后在某些if条件语句中不写unlock,导当然这个if条件语句执行到概率很低时,你的多线程代码跑好几天都不会出错。为了防止这类问题的发生,引入了std::lock_guard类模板。
lock_guard类模板可以帮助我们管理mutex这个锁类:当你忘记unlock解锁时,它会自动帮你unlock上!(保姆锁)
格式:
std::lock_guard guardObjName(mutexObjName);
//lock_guard的部分源码
template
class _NODISCARD lock_guard { // class with destructor that unlocks a mutex
public:
using mutex_type = _Mutex;
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock
~lock_guard() noexcept {
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
从源码中我们可以看出,
lock_guard类模板能帮助我们自动管理互斥量的本质原理:
①当lock_guard对象被构造出来时,会调用构造函数来使用对应互斥量的.lock()函数: _MyMutex.lock();
②当lock_guard对象被析构时(离开对应某函数的作用域时,即该函数return语句执行时),会调用析构函数来使用对应互斥量的.unlock()函数:_MyMutex.unlock();
注意:后续coding多线程代码时,可用lock_guard直接取代lock()和unlock(),从语法上讲,你使用了lock_guard后就不能再使用lock()和unlock()了。
小结:
lock_guard
lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()
这就是RAII机制下的一种类,RAII也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。
《3》死锁
死锁的概念解释:
C++中,死锁这个问题,是由至少2个锁头(也即2个互斥量)才能产生的。
比如我现在有2个线程A和B,对应2把锁头:金锁(Jinlock),银锁(Yinlock)
1-线程A执行时,这个线程先锁金锁,把金锁lock成功后,再开始尝试去lock银锁
2-线程B执行时,这个线程先锁银锁,因为银锁还没锁住,so线程B会把银锁lock成功,然后开始尝试去lock金锁3-线程A lock不上银锁,因为线程B死死地lock住了,此时代码就走不下去了
4-线程B lock不上金锁,因为线程A死死地lock住了,此时代码就走不下去了
5-大家都晾在这儿了,你等我,我等你,谁都不退一步!
这个死锁的过程可以比喻为:
1-张三站在北京等李四,李四不来就不挪窝了!
2-李四站在深圳等张三,张三不来就不挪窝了!
3-大家都尬住了!
3.1》死锁演示
#include
#include
#include
#include
using namespace std;
class A {
public:
//把收到的消息(玩家命令)插入到消息容器中的线程函数
void inMsgRecvQueue() {
//std::lock_guard sbguard(my_mutex);
for (int i = 0; i < 10000; i++) {
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
//std::lock_guard sbguard(my_mutex);
my_mutex.lock();
my_mutex2.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令
my_mutex2.unlock();
my_mutex.unlock();
}
return;
}
bool outMsgLULProc(int& command) {
//std::lock_guard sbguard(my_mutex);
my_mutex2.lock();
my_mutex.lock();
if (!msgRecvQueue.empty()) {
//消息容器不为空时
//int command = msgRecvQueue.front();//返回第一个元素,但是不检查元素是否存在
msgRecvQueue.pop_front();//移除消息容器中的首元素
//处理数据完毕...
my_mutex.unlock();
my_mutex2.unlock();
//sbguard.~lock_guard();
return true;
}
my_mutex.unlock();
my_mutex2.unlock();
//std::lock_guard sbguard(my_mutex);
return false;
}
//把数据从消息容器中取出的线程函数
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; i++) {
bool res = outMsgLULProc(command);
if (res){
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//处理数据完毕...
}
else {
//消息容器为空时
cout << "outMsgRecvQueue()执行,但是目前消息容器为空!(无消息)" << i << endl;
}
}
cout << "处理消息的容器执行完毕了!!!" << endl;
}
private:
list msgRecvQueue;//
mutex my_mutex;//创建一个互斥量!
mutex my_mutex2;//创建一个互斥量!
//因为2个互斥量才会造成你等我,我等你的尴尬局面!
};
int main(void) {
A mytobj;
thread Inthread(&A::inMsgRecvQueue, &mytobj);//&mytobj也可以写为std::ref(mytobj)
//也只有这样才能够传真正的引用进去到线程函数中!
thread Outthread(&A::outMsgRecvQueue, &mytobj);
Inthread.join();
Outthread.join();
cout << "主线程执行完毕,进程准备退出!" << endl;
return 0;
}
3.2》死锁的一般解决方案
way:只要保证多个互斥量上锁的顺序一样就不会造成死锁。
即:在不同的线程函数中,将2个互斥量.lock()和.unlock()的顺序保持一致!或者用std::lock_guard类模板定义两个互斥量时顺序保持一致!
3.3》std::lock()函数模板
std::lock()函数模板:能同时锁住2个或者2个以上的互斥量(至少2个,多了不限制,1个不行)。它能让我们避免在多个线程中使用互斥量时因为lock的顺序导致死锁的问题。(并且,该互斥量只有在处理多个互斥量时才出场!)
if互斥量中有一个没锁住,那么它就会在那等着,看能不能把其他的互斥量都锁住,只要发现锁不住,就会马上释放已经锁住的互斥量了。
格式:
std::lock(my_mutex1, my_mutex2,...);//互斥量之间的顺序可以任意拜访
demo_codes:
//在一个线程函数中:
std::lock(my_mutex1, my_mutex2);
//注意:std::lock()只是帮助你锁住多个互斥量而已,你还需要配合互斥量.unlock()函数来使用!
//处理其他线程函数中的行为codes
my_mutex1.unlock();
my_mutex2.unlock();
注意:即便你使用std::lock()函数解决了多个互斥量之间产生的死锁问题,但是你还是会面临着需要手动调用.unlock()函数释放互斥量(但你却忘记)了的问题。
我上述讲解过,lock_guard类模板可以解决你忘记调用.unlock()函数释放互斥量的问题。std::lock()函数模板又可以解决使用多个互斥量时产生的死锁问题,那么我们能不能把这2者的优势结合起来呢?
答:有的,please continue your reading!
3.4》std::lock_guard的std::adopt_lock参数
格式:
std::lock_guardstd::mutex my_guardName(my_mutexName,std::adopt_lock);
解释:
1-加入adopt_lock后,在调用lock_guard的构造函数时,不再对mutex互斥量对象进行lock();
2-adopt_lock为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
3-谨慎使用std::lock()对多个互斥量mutex对象进行锁定操作,wjw老师建议我们还是一个一个锁住即可。
demo_codes:
#include
#include
#include
#include
using namespace std;
class A {
public:
//把收到的消息(玩家命令)插入到消息容器中的线程函数
void inMsgRecvQueue() {
std::lock(my_mutex, my_mutex2);
std::lock_guard sbguard1(my_mutex,std::adopt_lock);
std::lock_guard sbguard2(my_mutex2,std::adopt_lock);
for (int i = 0; i < 10000; i++) {
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令
}
return;
}
bool outMsgLULProc(int& command) {
std::lock(my_mutex, my_mutex2);
std::lock_guard sbguard1(my_mutex, std::adopt_lock);
std::lock_guard sbguard2(my_mutex2, std::adopt_lock);
if (!msgRecvQueue.empty()) {
//消息容器不为空时
//int command = msgRecvQueue.front();//返回第一个元素,但是不检查元素是否存在
msgRecvQueue.pop_front();//移除消息容器中的首元素
//处理数据完毕...
return true;
}
return false;
}
//把数据从消息容器中取出的线程函数
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 10000; i++) {
bool res = outMsgLULProc(command);
if (res)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//处理数据完毕...
}
else {
//消息容器为空时
cout << "outMsgRecvQueue()执行,但是目前消息容器为空!(无消息)" << i << endl;
}
}
cout << "处理消息的容器执行完毕了!!!" << endl;
}
private:
list msgRecvQueue;//
mutex my_mutex;//创建一个互斥量!
mutex my_mutex2;//创建一个互斥量!
};
int main(void) {
A mytobj;
thread Inthread(&A::inMsgRecvQueue, &mytobj);//&mytobj也可以写为std::ref(mytobj)
//也只有这样才能够传真正的引用进去到线程函数中!
thread Outthread(&A::outMsgRecvQueue, &mytobj);
Inthread.join();
Outthread.join();
cout << "主线程执行完毕,进程准备退出!" << endl;
return 0;
}