一、互斥量(mutex)的基本概念
1.我们为什么需要互斥量
这就涉及到了共享资源的概念,例如:买票这一行为,(票就是我们的共享资源)
假设有两个窗口 1 和 2号窗口(可以看成是两个"售票"子线程),
这时有两人同时购买从上海到北京的票,同样挑中了A1座,如果这时不对这类
行为做任何处理,就会造成两个人同时买到A1票的错误反馈。
保护共享数据,操作时,某个线程 用代码把共享数据锁住->操作数据->解锁;其他想操作共享数据的线程必须等待解锁->锁定住->操作->解锁;
以上种种引出 “互斥量”
2.互斥量(mutex)的基本概念
- 互斥量是个类对象。可以理解成一把锁,多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标志是lock()函数返回)
- 如果没有锁成功,那么流程卡在lock()这里不断的尝试去锁这把锁头;
- 互斥量使用要小心,保护数据不多也不少,少了,没达到保护效果,多了影响效率;
二、互斥量的用法
2.1 lock(),unlock()
mutex互斥量是一个类,这个类有一个lock()方法,和一个unlock()方法(成员函数)。
(1)引入头文件
#include <mutex>
using namespace std;
(2)lock()和unlock()
lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
步骤:1.先lock();
2.操作共享数据
3.unlock();
lock()和unlock()要成对使用,有lock()必然要有unlock(),
每调用一次lock(),必然应该调用一次unlock();
不应该也不允许调用1次lock()却调用了2次unlock(),也不允许
调用2次lock却调用1次unlock,这些非对称数量的调用都会导致
代码不稳定甚至崩溃。
(3)测试代码
// project6.cpp : 定义控制台应用程序的入口点。
//
//网络游戏服务器
#include "stdafx.h"
#include <iostream>
#include <stdio.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
//可以把需要加锁的代码提取成一个函数,方便加锁
class A {
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
my_mutex.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是服务器收到的命令,直接弄到消息队列里面来
my_mutex.unlock();//成对使用,在写的时候保护一下
}//从后塞
return;
}
把数据从消息队列中取出的线程
//void outMsgRecvQueue()
//{
// for (int i = 0; i < 100000; ++i)
// {
// if (!msgRecvQueue.empty())
// {
// //消息不为空
// //front() 返回第一个元素,但不检查元素是否存在;
// int command = msgRecvQueue.front();//从头取
// msgRecvQueue.pop_front();//移除第一个元素,但不返回
// //这里就要考虑处理数据...
// //......
// }
// else
// {
// //消息队列为空
// cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
// }
// }
// cout << "end" << endl;
//}
//方便处理共享数据
bool outMsgLULProc(int &command)//引用
{
//必须是同一个互斥量的锁与不锁
my_mutex.lock();
//消息队列不为空取命令(判断空不空也是对共享数据的访问,所以要加锁)
if (!msgRecvQueue.empty())
{
//消息不为空
//front() 返回第一个元素,但不检查元素是否存在;
command = msgRecvQueue.front();//从头取
msgRecvQueue.pop_front();//移除第一个元素,但不返回
my_mutex.unlock();//一一对应 一次lock 一次unlock
return true;
//两个出口必然需要两个unlock
//每一个分支往外退就得有一个unlock
}
my_mutex.unlock();//解锁
return false;//为空返回失败
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool results = outMsgLULProc(command);
if (results == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)处理
//.....
}
else
{
//消息队列为空
cout << "outMsgQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
//共享数据
std::list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家给服务器发送过来的命令。
std::mutex my_mutex;//创建了一个互斥量
};
int main()
{
A myobja;
//类成员函数
//第二个参数是 引用,才能保证线程里用的是同一个对象myobja。
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
//分析问题:
//inMsgRecvQueue 不断的往容器里面写数据
//outMsgRecvQueue 不断往容器里面读数据
//两个线程有读有写,若是完全不控制随意运行就会有问题
//就得按顺序按规矩去访问共享数据
//代码化解决问题:
//引入一个C++解决多线程保护共享数据问题的第一个概念“互斥量”。
myOutnMsgObj.join();
myInMsgObj.join();
return 0;
}
(4)运行结果
(5)分析总结
- 有lock,忘记unlock的问题,非常难排查;
- 为了防止忘记unlock(),引入了一个叫std::lock_guard的类模板;忘记了加unlock,此类模板会帮我们unlock
- 就像智能指针(unique_ptr()):忘记释放内存,会帮我们释放(保姆)
2.2 std::lock_quard类模板
std::lock_guard类模板:直接取代lock()和unlock();
即使用了lock_guard后,再也不能使用lock()和unlock()了
(1)测试代码
//网络游戏服务器
#include "stdafx.h"
#include <iostream>
#include <stdio.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
//可以把需要加锁的代码提取成一个函数,方便加锁
class A {
public:
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
{//让其再每一次for之后都析构(有意义:在每一次for结束之前还有很长的一段处理代码)
std::lock_guard<std::mutex> sbguard(my_mutex);
//my_mutex.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是服务器收到的命令,直接弄到消息队列里面来
}//提前解锁,提前结束sbguard的生命周期
//my_mutex.unlock();//成对使用,在写的时候保护一下
//从后塞
//......
//其他处理代码;(很长很久)
}//如果不提前就会在,就会在这里(所有的100000次之后)结束生命周期
return;
}
//方便处理共享数据
bool outMsgLULProc(int &command)//引用
{
//std::lock_guard :类模板
//std::mutex 类型
//sbguard : 对象名
//my_mutex :互斥量
std::lock_guard<std::mutex> sbguard(my_mutex);
//什么工作原理?
//1.sbguard生成这样的对象,调用这个类的构造函数(在这个类的构造函数里就执行了lock())
//lock_guard构造函数里执行了mutex::lock();
//2.sbguard是局部对象return 退出函数时局部对象超过作用域会析构
//lock_guard析构函数里执行了mutex::unlock();
//3.可以用{}(作用域)的方式来提前结束sbguard的生命周期;提前解锁,不必等到return再解锁
//必须是同一个互斥量的锁与不锁
//my_mutex.lock();
//消息队列不为空取命令(判断空不空也是对共享数据的访问,所以要加锁)
if (!msgRecvQueue.empty())
{
//消息不为空
//front() 返回第一个元素,但不检查元素是否存在;
command = msgRecvQueue.front();//从头取
msgRecvQueue.pop_front();//移除第一个元素,但不返回
//my_mutex.unlock();//一一对应 一次lock 一次unlock
return true;
//两个出口必然需要两个unlock
//每一个分支往外退就得有一个unlock
}
//my_mutex.unlock();//解锁
return false;//为空返回失败
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool results = outMsgLULProc(command);
if (results == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)处理
//.....
}
else
{
//消息队列为空
cout << "outMsgQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
//共享数据
std::list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家给服务器发送过来的命令。
std::mutex my_mutex;//创建了一个互斥量
};
int main()
{
A myobja;
//类成员函数
//第二个参数是 引用,才能保证线程里用的是同一个对象myobja。
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutnMsgObj.join();
myInMsgObj.join();
return 0;
}
(2)运行结果图
(3)总结
- 一个互斥量就是一把"锁头"
三、死锁
例如:
生活中
张三:站在北京等李四来才走,但是一个人时不动
李四:站在上海等张三来才走,但是一个人时不动
这就造成了互相等待,互相不行动
在C++中
假设现在有两把锁(死锁这个问题 是由至少两个“锁头”也就是两个互斥量才能产生);金锁(JinLock),银锁(YinLock);
两个线程A,B
(1)线程A执行的时候,这个线程先锁 金锁,把金锁lock()成功了,然后它去lock银锁。。。
这时出现了上下文切换
(2)(切换到了线程B)线程B执行了,这个线程先锁 银锁,因为银锁还没有被锁,所以银锁会lock()成功,接着线程B要去lock金锁…
此时此刻,死锁就产生了;
(3)线程A因为拿不到银锁头,流程走不下去(所有后边代码有unlock金锁头的但是流程走不下去,所以金锁头解不开);
(4)线程B因为拿不到金锁头,流程走不下去(所有后边代码有unlock银锁头的但是流程走不下去,所以银锁头解不开);
这样两个线程都只能停在这里,A等B,B等A;
这就是死锁。
3.1 死锁演示
(1)代码演示
//网络游戏服务器
#include "stdafx.h"
#include <iostream>
#include <stdio.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
//可以把需要加锁的代码提取成一个函数,方便加锁
class A {
public:
//inMsgRecvQueue 先锁1后锁2
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++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)//引用
{
//死锁:这里锁的顺序相反
my_mutex2.lock();
my_mutex1.lock();
//消息队列不为空取命令(判断空不空也是对共享数据的访问,所以要加锁)
if (!msgRecvQueue.empty())
{
//消息不为空
//front() 返回第一个元素,但不检查元素是否存在;
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 results = outMsgLULProc(command);
if (results == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)处理
//.....
}
else
{
//消息队列为空
cout << "outMsgQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
//共享数据
std::list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家给服务器发送过来的命令。
std::mutex my_mutex1;//创建了一个互斥量
std::mutex my_mutex2;//创建了一个互斥量(两把锁头)
};
int main()
{
A myobja;
//类成员函数
//第二个参数是 引用,才能保证线程里用的是同一个对象myobja。
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutnMsgObj.join();
myInMsgObj.join();
return 0;
}
(2)运行结果图
unlock的顺序不重要
3.2 死锁的一般解决方案
(1) 只要保证这两个互斥量上锁的顺序一致就不会死锁【代码+运行结果】
//网络游戏服务器
#include "stdafx.h"
#include <iostream>
#include <stdio.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
//可以把需要加锁的代码提取成一个函数,方便加锁
class A {
public:
//inMsgRecvQueue 先锁1后锁2
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++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)//引用
{
//死锁:这里锁的顺序相反
my_mutex1.lock();
my_mutex2.lock();
//消息队列不为空取命令(判断空不空也是对共享数据的访问,所以要加锁)
if (!msgRecvQueue.empty())
{
//消息不为空
//front() 返回第一个元素,但不检查元素是否存在;
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 results = outMsgLULProc(command);
if (results == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)处理
//.....
}
else
{
//消息队列为空
cout << "outMsgQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
//共享数据
std::list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家给服务器发送过来的命令。
std::mutex my_mutex1;//创建了一个互斥量
std::mutex my_mutex2;//创建了一个互斥量(两把锁头)
};
int main()
{
A myobja;
//类成员函数
//第二个参数是 引用,才能保证线程里用的是同一个对象myobja。
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutnMsgObj.join();
myInMsgObj.join();
return 0;
}
3.3 std::lock()函数模板
std::lock()函数模板:用来处理多个互斥量
功能:(同时)一次锁住两个或者两个以上的互斥量(至少两个,多了不行,1个也不行);
它不存在这种在多个线程中 因为锁的顺序问题导致死锁的风险问题;
std::lock():如果互斥量中有一个没锁住,它就在那里等着,等所有互斥量都锁住,它才能往下走(返回);
要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。
保证不会出现死锁,不管锁的顺序。
(1)代码测试
//网络游戏服务器
#include "stdafx.h"
#include <iostream>
#include <stdio.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
//可以把需要加锁的代码提取成一个函数,方便加锁
class A {
public:
//inMsgRecvQueue 先锁1后锁2
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::lock(my_mutex1, my_mutex2);//相当于每个互斥量都调用了.lock();
msgRecvQueue.push_back(i);//假设这个数字i就是服务器收到的命令,直接弄到消息队列里面来
//前面锁住2个,后面就得给两个解锁
my_mutex2.unlock();
my_mutex1.unlock();
}
return;
}
//方便处理共享数据
bool outMsgLULProc(int &command)//引用
{
std::lock(my_mutex1, my_mutex2);
//消息队列不为空取命令(判断空不空也是对共享数据的访问,所以要加锁)
if (!msgRecvQueue.empty())
{
//消息不为空
//front() 返回第一个元素,但不检查元素是否存在;
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 results = outMsgLULProc(command);
if (results == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)处理
//.....
}
else
{
//消息队列为空
cout << "outMsgQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
//共享数据
std::list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家给服务器发送过来的命令。
std::mutex my_mutex1;//创建了一个互斥量
std::mutex my_mutex2;//创建了一个互斥量(两把锁头)
};
int main()
{
A myobja;
//类成员函数
//第二个参数是 引用,才能保证线程里用的是同一个对象myobja。
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutnMsgObj.join();
myInMsgObj.join();
return 0;
}
3.4 std::lcok_guard的std::adopt_lock参数
std::adopt_lock是个结构体对象,起一个标记作用:
作用就是表示这个互斥量已经lock()了,不需要在std::lock_guard< std::mutex >构造函数里面再对mutex对象进行lock()了;
std::lock_guard< std::mutex >析构时功能是正常的。
(1)代码测试
//网络游戏服务器
#include "stdafx.h"
#include <iostream>
#include <stdio.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
//可以把需要加锁的代码提取成一个函数,方便加锁
class A {
public:
//inMsgRecvQueue 先锁1后锁2
//把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::lock(my_mutex1, my_mutex2);//相当于每个互斥量都调用了.lock();
//std::adopt_lock 让sqm1对象构造的时候,不调用 mutex.lock() 这就避免了冲突
//这样在析构的时候,也能帮忙调用unlock,而无需我们手动去写
std::lock_guard<std::mutex> sqm1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> sqm2(my_mutex2, std::adopt_lock);
msgRecvQueue.push_back(i);//假设这个数字i就是服务器收到的命令,直接弄到消息队列里面来
//前面锁住2个,后面就得给两个解锁
//my_mutex2.unlock();
//my_mutex1.unlock();
}
return;
}
//方便处理共享数据
bool outMsgLULProc(int &command)//引用
{
std::lock(my_mutex1, my_mutex2);
//消息队列不为空取命令(判断空不空也是对共享数据的访问,所以要加锁)
std::lock_guard<std::mutex> sqm1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> sqm2(my_mutex2, std::adopt_lock);
if (!msgRecvQueue.empty())
{
//消息不为空
//front() 返回第一个元素,但不检查元素是否存在;
command = msgRecvQueue.front();//从头取
msgRecvQueue.pop_front();//移除第一个元素,但不返回
return true;
}
return false;//为空返回失败
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool results = outMsgLULProc(command);
if (results == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)处理
//.....
}
else
{
//消息队列为空
cout << "outMsgQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
//共享数据
std::list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家给服务器发送过来的命令。
std::mutex my_mutex1;//创建了一个互斥量
std::mutex my_mutex2;//创建了一个互斥量(两把锁头)
};
int main()
{
A myobja;
//类成员函数
//第二个参数是 引用,才能保证线程里用的是同一个对象myobja。
std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutnMsgObj.join();
myInMsgObj.join();
return 0;
}
(2)运行结果
3.5 总结
- std::lock():一次锁定多个互斥量;谨慎使用(建议一个一个锁,因为一般,多个互斥量有多个要保护的东西(保护不同的东西),很少会挨在一起,同时锁定)