互斥量
上一篇文章的最后的示例:
假设做一个简易的网络游戏服务器:
两个自己创建的线程,一个线程收集玩家命令(用数字代表),并将命令写到一个队列中
另一个线程从队列中取出玩家发送来的命令,解析,然后执行玩家需要的动作
上一篇文章最后的示例演示发现如下问题:
程序运行途中会出现崩溃,原因就是不断地往容器中写数据,同时不停地读数据,两个线程随意运行,一定会出问题。
假设写线程正在执行,而读线程又去读数据并删除数据,程序乱套,报出异常。
解决思路:在执行写数据线程的时候,让读数据线程等着。相反的情况就是,在执行读线程的时候,写数据线程等待。读完后再往这块共享内存中写数据。
解决办法:
保护共享数据,操作时,用代码把共享数据锁住,再操作数据、最后解锁
其他想操作共享数据的线程必须等待解锁,然后再锁住,操作数据,再解锁
一、互斥量(mutex)的基本概念
互斥量是个类对象,通俗理解为一把锁,多个线程尝试用lock()加锁,只有一个线程能够锁定成功(lock()返回)
如果没锁成功,那么流程就卡在lock()这里不断尝试去给这把锁上锁
注意:互斥量(锁)保护数据不能多也不能少,少了,没达到保护效果,多了,影响效率
二、互斥量用法
2.1)lock(),unlock()
步骤:先lock(),操作共享数据,再unlock()
lock()和unlock()要成对使用,有lock()必然要有unlock(),没调用一次lock(),必然应该调用一次unlock();
为了防止忘记unlock(),引入了一个叫std::lock_guard()的类模板:可以实现智能unlock(),类似于智能指针unique_ptr<>
2.2)std::lock_guard类模板可以取代lock()和unlock(),用了lock_guard后,不能再使用lock()和unlock()
lock_guard构造函数里执行了mutex::lock(),析构函数里执行了mutex::unlock()
小技巧:可以加个{}提前析构lock_guard,实现提前unlock()
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
using namespace std;
#include <list>
//用成员函数作为线程函数的方法来写线程
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
{
std::lock_guard<std::mutex> myGuard(myMutex);
//myMutex.lock(); //加锁
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
//myMutex.unlock(); //解锁
} //加个{}可以提前执行lock_guard的析构函数,即执行unlock()**********小技巧**********
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
std::lock_guard<std::mutex> mutexGuard(myMutex); //创建lock_guard对象
//myMutex.lock();
//加锁(使用了lock_guard,下面就可以不用使用lock和unlock)
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
//.............
//myMutex.unlock(); //消息不为空时解锁
return true;
}
//myMutex.unlock(); //消息为空时解锁
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex; //创建了一个互斥量
};
int main(int argc, char** argv)
{
A obj;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
//第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)
std::thread myInMsgObj(&A::inMsgRecvQueue, &obj); //第二个参数未引用
myOutMsgObj.join(); //阻塞主线程并等待子线程执行完毕
myInMsgObj.join();
return 0;
}
该代码不会像上一篇的示例(不定时出现程序崩溃),而是一直正常运行,可自行测试
三、死锁(至少两把锁(互斥量)才会产生死锁)
一个互斥量相当于一把锁,如果有两把锁(锁1和锁2),并且有两个线程(线程A和线程B)
1)线程A执行的时候先锁 锁1,即锁1.lock()并且成功,然后要去lock锁2
途中出现了上下文切换,线程B执行
2)线程B执行了,这个线程先锁 锁2,因为锁2还没有被锁,所以锁2会在此线程中lock()成功,然后线程B要去lock锁1...
此时死锁产生
3)线程A拿不到锁2,流程无法继续(所有后边的程序有解锁锁1的,但是流程执行不下去,所以锁1无法解开)
4)线程B拿不到锁1,流程无法继续(所有后边的程序有解锁锁2的,但是流程执行不下去,所以锁2无法解开)
大家都卡在这里,
3.1)下面为产生死锁的程序演示:
用成员函数作为线程函数的方法来写线程
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
myMutex1.lock(); //先加锁1
// .... //执行其他操作
myMutex2.lock(); //后加锁2
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
myMutex2.unlock(); //先解锁2
//..... //执行其他操作
myMutex1.unlock(); //后解锁1
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
myMutex2.lock(); //先加锁2
myMutex1.lock(); //后加锁1
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
//.............
myMutex1.unlock(); //先解锁1
myMutex2.unlock(); //后解锁2
return true;
}
myMutex1.unlock(); //先解锁1
myMutex2.unlock(); //后解锁1
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex1; //创建了一个互斥量(锁1)
mutex myMutex2; //创建另外一个互斥量(锁2)
};
int main(int argc, char** argv)
{
A obj;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
//第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)
std::thread myInMsgObj(&A::inMsgRecvQueue, &obj); //第二个参数未引用
myOutMsgObj.join(); //阻塞主线程并等待子线程执行完毕
myInMsgObj.join();
return 0;
}
执行如上程序,可以发现程序不定时出现卡死现象,即死锁现象产生了,如下图所示
3.2)死锁的一般解决方案
只要保证两个线程中两个互斥量(锁)的上锁顺序一致就不会死锁
比如,可以将其中一个线程上锁顺序与另一个线程一致,这里,下面的程序先加锁1,再加锁2,代码如下。
bool outMsgProc(int& command)
{
myMutex1.lock(); //先加锁1
myMutex2.lock(); //后加锁2
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
//.............
myMutex1.unlock(); //先解锁1
myMutex2.unlock(); //后解锁2
return true;
}
myMutex1.unlock(); //先解锁1
myMutex2.unlock(); //后解锁1
return false;
}
重新运行程序,发现不会出现死锁现象,即程序运行一致不会卡死。
3.3)std::lock()函数模板(用来处理多个互斥量)
可以一次性锁住至少两个互斥量,而一个不行
不存在在多个线程中由于多个互斥量(锁)上锁的顺序问题而导致死锁现象产生
如果互斥量中有一个没锁住,就unlock已经锁住的,然后不断再次尝试lock所有的互斥量,等所有互斥量都锁住,它才往下走(返回)
比如该例子中,要么两个互斥量都锁住,要么两个互斥量都没锁住
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
//myMutex1.lock(); //先加锁1
// .... //执行其他操作
//myMutex2.lock(); //后加锁2
//改进:
std::lock(myMutex1, myMutex2);//相当于两个互斥量同时lock
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
myMutex2.unlock(); //先解锁2
//..... //执行其他操作
myMutex1.unlock(); //后解锁1
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
//myMutex2.lock(); //先加锁2
//myMutex1.lock(); //后加锁1
//改进
std::lock(myMutex1, myMutex2); //相当于两个互斥量同时lock
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
//.............
myMutex1.unlock(); //先解锁1
myMutex2.unlock(); //后解锁2
return true;
}
myMutex1.unlock(); //先解锁1
myMutex2.unlock(); //后解锁1
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex1; //创建了一个互斥量(锁1)
mutex myMutex2; //创建另外一个互斥量(锁2)
};
int main(int argc, char** argv)
{
A obj;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj); //第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)
std::thread myInMsgObj(&A::inMsgRecvQueue, &obj); //第二个参数未引用
myOutMsgObj.join(); //阻塞主线程并等待子线程执行完毕
myInMsgObj.join();
return 0;
}
运行上面的程序,不会出现死锁。其实上面两个线程中只要有一个使用了std::lock()便不会出现死锁现象(上面两个线程中都使用了std::lock())。
3.4)避免忘记unlock
遗憾:使用std::lock()后还是要面临忘记unlock()的问题
解决:使用std::lock_guard的std::adopt_lock参数
std::adopt_lock是个结构体对象,起标记作用,表示互斥量已经lock(),不需要在std::lock_guard<std::mutex>里面对mutex对象进行lock().
std::lock()一次锁定多个互斥量:谨慎使用(建议一个一个lock())
class A
{
public:
//线程:把收到的消息(玩家命令)放入一个队列
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue(),插入一个元素" << i << endl;
//myMutex1.lock(); //先加锁1
// .... //执行其他操作
//myMutex2.lock(); //后加锁2
//改进:
std::lock(myMutex1, myMutex2);//相当于两个互斥量同时lock
std::lock_guard<std::mutex> myGuard1(myMutex1, std::adopt_lock); //使用这个避免忘记unlock
std::lock_guard<std::mutex> myGuard2(myMutex2, std::adopt_lock);
msgRecvQueue.push_back(i); //假设i就是收到的命令,存入消息队列
//myMutex2.unlock(); //先解锁2
//执行其他操作
//myMutex1.unlock(); //后解锁1
}
return;
}
//判断消息是否为空,不为空取出命令
bool outMsgProc(int& command)
{
//myMutex2.lock(); //先加锁2
//myMutex1.lock(); //后加锁1
//改进:
std::lock(myMutex1, myMutex2); //相当于两个互斥量同时lock
std::lock_guard<std::mutex> myGuard1(myMutex1, std::adopt_lock);//使用这个避免忘记unlock
std::lock_guard<std::mutex> myGuard2(myMutex2, std::adopt_lock);
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
//处理取出的数据
//.............
//myMutex1.unlock(); //先解锁1
//上面使用了adopt_lock参数,此处不必unlock
//myMutex2.unlock(); //后解锁2
return true;
}
//myMutex1.unlock(); //先解锁1
//myMutex2.unlock(); //后解锁1
return false;
}
//线程:把收到的命令取出
void outMsgRecvQueue()
{
int command;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
//处理命令
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //存放玩家发送的命令队列(共享数据)
mutex myMutex1; //创建了一个互斥量(锁1)
mutex myMutex2; //创建另外一个互斥量(锁2)
};
int main(int argc, char** argv)
{
A obj;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
//第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)
std::thread myInMsgObj(&A::inMsgRecvQueue, &obj); //第二个参数未引用
myOutMsgObj.join(); //阻塞主线程并等待子线程执行完毕
myInMsgObj.join();
return 0;
}














网友评论