前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UNIX(多线程):06---互斥量概念、用法、死锁演示及解决详解

UNIX(多线程):06---互斥量概念、用法、死锁演示及解决详解

作者头像
用户3479834
发布2021-02-03 14:26:28
6040
发布2021-02-03 14:26:28
举报
文章被收录于专栏:游戏开发司机

互斥量(mutex)的基本概念

  • 保护共享大数据,操作时,某个线程 用代码把共享数据锁住、操作数据、解锁,其他想操作共享数据的线程必须等待解锁,锁定住,操作,解锁。
  • 互斥量是个类对象。
    • 理解成一把锁,多个线程尝试用 lock() 成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标志是lock()函数返回)。
    • 如果没锁成功,那么流程阻塞在lock()这里不断的尝试去锁这把锁头。
  • 互斥量使用要小心,保护数据不多也不少,少了,没达到保护效果,多了,影响效率。

互斥量的用法

  • 引入头文件 #include <mutex>;

lock(), unlock()

  • 步骤:先lock(), 操作共享数据,再unlock()。
  • lock()和unlock()要成对使用,有lock必然要有unlock,每调用一次lock(),必然应该调用一次unlock()。
    • 不应该也不允许调用1次lock()却调用了2次unlock(),也不允许调用2次lock却调用1次unlock(),这些非对称。
  • 数量的调用都会导致代码不稳定甚至崩溃。有lock,忘记unlock的问题,非常难排查。
  • 如果lock了,注意退出的地方(如 return)是不是加上了unlock,几个出口几个unlock。
代码语言:javascript
复制
代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class A{
public:
//把收到的消息(玩家命令)加入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 1; i < 10000; ++i)
{
cout << "inMsgRecvQueue执行了,插入一个元素" << i << endl;
my_mutex.lock();
msgRecvQueue.push_back(i);    //假设这个数字就是玩家发来的命令,加入到消息队列中
my_mutex.unlock();
}
}
//在这个函数中加锁
bool outMsgMutPro(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{};
for (int i = 1; i < 10000; ++i)
{
bool ret = outMsgMutPro(command);
if (ret)
{
cout << "outMsgMutPro执行了,取出一个元素" << command << endl;
//这里就针对具体的命令具体处理
//...
}
else {
//消息队列为空
cout << "outMsgRecvQueue执行了,但是当前消息队列为空" << i << endl;
}
}
cout << "outMsgRecvQueue()执行完毕" << endl;
}
private:
std::list<int> msgRecvQueue;  //容器(消息队列),专门代表玩家给我们发来的命令
std::mutex my_mutex;
};
int main()
{
A obja;
std::thread outMsgThread(&A::outMsgRecvQueue, &obja); //第二个参数是引用,保证线程里操作同一个对象
std::thread inMsgThread(&A::inMsgRecvQueue, &obja);
inMsgThread.join();
outMsgThread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}

std::lock_guard类模板

  • 为了防止大家忘记unlock(),引入了一个叫std::lock_guard的类模板:你忘记unlock不要紧,我替你unlock()。
    • 联想智能指针(unique_ptr<>) : 你忘记释放内存不要紧,我给你释放。
  • std::lock_guard 类模板:直接取代lock() 和unlock();也就是说, 你用了lock_guard之后,再不能使用lock()和unlock()了。
    • lock_guard构造函数里执行了mutex::lock()。
    • lock_guard析构函数里执行了mutex::unlock()。
    • 结合 {} ,可以控制作用的范围(RAII)。
代码语言:javascript
复制
代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class A{
public:
//把收到的消息(玩家命令)加入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 1; i < 10000; ++i)
{
cout << "inMsgRecvQueue执行了,插入一个元素" << i << endl;
{
std::lock_guard<mutex> mutex_guard_in(my_mutex);
msgRecvQueue.push_back(i);    //假设这个数字就是玩家发来的命令,加入到消息队列中
}
//其他代码...
}
}
//在这个函数中加锁
bool outMsgMutPro(int& command )
{
std::lock_guard<mutex> mutex_guard_out(my_mutex);
if (!msgRecvQueue.empty())
{
//消息队列不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();           //移除第一个元素,但不返回
return true;
}
return false;
}
//把消息从消息队列中取出的线程
void outMsgRecvQueue()
{
int command{};
for (int i = 1; i < 10000; ++i)
{
bool ret = outMsgMutPro(command);
if (ret)
{
cout << "outMsgMutPro执行了,取出一个元素" << command << endl;
//这里就针对具体的命令具体处理
//...
}
else {
//消息队列为空
cout << "outMsgRecvQueue执行了,但是当前消息队列为空" << i << endl;
}
}
cout << "outMsgRecvQueue()执行完毕" << endl;
}
private:
std::list<int> msgRecvQueue;  //容器(消息队列),专门代表玩家给我们发来的命令
std::mutex my_mutex;
};
int main()
{
A obja;
std::thread outMsgThread(&A::outMsgRecvQueue, &obja); //第二个参数是引用,保证线程里操作同一个对象
std::thread inMsgThread(&A::inMsgRecvQueue, &obja);
inMsgThread.join();
outMsgThread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}

死锁

  • 比如我有两把锁(死锁这个问题 是由至少两个锁头也就是两个互斥量才能产生):金锁(jinlock) 银锁(yinlock)。
  • 两个线程 A,B
    • (1) 线程A执行的时候,这个线程先锁金锁,把金锁lock()成功了,然后它去lock银锁。
    • 出现了上下文切换
    • (2) 线程B执行了,这个线程先锁银锁,因为银锁还没有被锁,所以银锁会lock()成功,线程B要去lock金锁。
    • 此时此刻,死锁就产生了。
    • (3) 线程A因为拿不到银锁头,流程走不下去(所有后边代码有解锁金锁锁头的但是流程走不下去,所以金锁头解不开)。
    • (4) 线程B因为拿不到金锁头,流程走不下去(所有后边代码有解锁银锁锁头的但是流程走不下去,所以银锁头解不开)。
    • 大家都晾在这里,你等我,我等你。

死锁演示

代码语言:javascript
复制
代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class A
{
public:
//把收到的消息(玩家命令)加入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 1; i < 10000; ++i)
{
cout << "inMsgRecvQueue执行了,插入一个元素" << i << endl;
my_mutex2.lock();
//其他代码
my_mutex1.lock();
msgRecvQueue.push_back(i);    //假设这个数字就是玩家发来的命令,加入到消息队列中
my_mutex1.unlock();
//其他代码
my_mutex2.unlock();
}
}
//在这个函数中加锁
bool outMsgMutPro(int& command )
{
my_mutex1.lock();
//其他代码
my_mutex2.lock();
if (!msgRecvQueue.empty())
{
//消息队列不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();           //移除第一个元素,但不返回
my_mutex2.unlock();
//其他代码
my_mutex1.unlock();
return true;
}
my_mutex2.unlock();
//其他代码
my_mutex1.unlock();
return false;
}
//把消息从消息队列中取出的线程
void outMsgRecvQueue()
{
int command{};
for (int i = 1; i < 10000; ++i)
{
bool ret = outMsgMutPro(command);
if (ret)
{
cout << "outMsgMutPro执行了,取出一个元素" << command << endl;
//这里就针对具体的命令具体处理
//...
}
else {
//消息队列为空
cout << "outMsgRecvQueue执行了,但是当前消息队列为空" << i << endl;
}
}
cout << "outMsgRecvQueue()执行完毕" << endl;
}
private:
std::list<int> msgRecvQueue;  //容器(消息队列),专门代表玩家给我们发来的命令
std::mutex my_mutex1;
std::mutex my_mutex2;
};
int main()
{
A obja;
std::thread outMsgThread(&A::outMsgRecvQueue, &obja); //第二个参数是引用,保证线程里操作同一个对象
std::thread inMsgThread(&A::inMsgRecvQueue, &obja);
inMsgThread.join();
outMsgThread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}

死锁的一般解决方案

  • 只要保证这两个互斥量上锁的顺序一致就不会死锁。

std::lock()函数模板

  • 用来处理多个互斥量的时候才出场。
  • 能力:一次锁住两个或者两个以上的互斥量(至少两个,多了不限,1个不行)。
  • 它不存在这种因为再多个线程中 因为锁的顺序问题导致死锁的风险问题。
  • std::lock():
    • 如果互斥量中有一个没锁住,它就在那里等着,等所有互斥量都锁住,它才能往下走(返回)。
    • 要么两个互斥量都锁住,要么两个互斥量都没锁住。
    • 如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。
代码语言:javascript
复制
代码语言:javascript
复制
class A
{
public:
//把收到的消息(玩家命令)加入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 1; i < 10000; ++i)
{
cout << "inMsgRecvQueue执行了,插入一个元素" << i << endl;
std::lock(my_mutex1, my_mutex2);
msgRecvQueue.push_back(i);    //假设这个数字就是玩家发来的命令,加入到消息队列中
my_mutex1.unlock();
//其他代码
my_mutex2.unlock();
}
}
//在这个函数中加锁
bool outMsgMutPro(int& command )
{
std::lock(my_mutex1, my_mutex2);
if (!msgRecvQueue.empty())
{
//消息队列不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();           //移除第一个元素,但不返回
my_mutex2.unlock();
//其他代码
my_mutex1.unlock();
return true;
}
my_mutex2.unlock();
//其他代码
my_mutex1.unlock();
return false;
}
//把消息从消息队列中取出的线程
void outMsgRecvQueue()
{
int command{};
for (int i = 1; i < 10000; ++i)
{
bool ret = outMsgMutPro(command);
if (ret)
{
cout << "outMsgMutPro执行了,取出一个元素" << command << endl;
//这里就针对具体的命令具体处理
//...
}
else {
//消息队列为空
cout << "outMsgRecvQueue执行了,但是当前消息队列为空" << i << endl;
}
}
cout << "outMsgRecvQueue()执行完毕" << endl;
}
private:
std::list<int> msgRecvQueue;  //容器(消息队列),专门代表玩家给我们发来的命令
std::mutex my_mutex1;
std::mutex my_mutex2;
};

std::lock_guard的std::adopt_lock参数

  • std::adopt_lock是个结构体对象,起一个标记作用:作用就是表示这个互斥量已经lock(),不需要再std::lock_guard<std::mutext>构造函数里 再面对对象进行再次lock()了。
代码语言:javascript
复制
代码语言:javascript
复制
class A
{
public:
//把收到的消息(玩家命令)加入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 1; i < 10000; ++i)
{
cout << "inMsgRecvQueue执行了,插入一个元素" << i << endl;
std::lock(my_mutex1, my_mutex2);
std::lock_guard<mutex> in_mutex_guard1(my_mutex1, std::adopt_lock);
std::lock_guard<mutex> in_mutex_guard2(my_mutex2, std::adopt_lock);
msgRecvQueue.push_back(i);    //假设这个数字就是玩家发来的命令,加入到消息队列中
//其他代码
}
}
//在这个函数中加锁
bool outMsgMutPro(int& command )
{
std::lock(my_mutex1, my_mutex2);
std::lock_guard<mutex> out_mutex_guard1(my_mutex1, std::adopt_lock);
std::lock_guard<mutex> out_mutex_guard2(my_mutex2, std::adopt_lock);
if (!msgRecvQueue.empty())
{
//消息队列不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();           //移除第一个元素,但不返回
return true;
}
return false;
}
//把消息从消息队列中取出的线程
void outMsgRecvQueue()
{
int command{};
for (int i = 1; i < 10000; ++i)
{
bool ret = outMsgMutPro(command);
if (ret)
{
cout << "outMsgMutPro执行了,取出一个元素" << command << endl;
//这里就针对具体的命令具体处理
//...
}
else {
//消息队列为空
cout << "outMsgRecvQueue执行了,但是当前消息队列为空" << i << endl;
}
}
cout << "outMsgRecvQueue()执行完毕" << endl;
}
private:
std::list<int> msgRecvQueue;  //容器(消息队列),专门代表玩家给我们发来的命令
std::mutex my_mutex1;
std::mutex my_mutex2;
};
  • std::lock():一次锁定多个互斥量,谨慎使用(建议一个一个锁)。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 游戏开发司机 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 互斥量(mutex)的基本概念
    • 互斥量的用法
      • lock(), unlock()
      • std::lock_guard类模板
    • 死锁
      • 死锁演示
      • 死锁的一般解决方案
      • std::lock()函数模板
      • std::lock_guard的std::adopt_lock参数
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档