目录
1.互斥量的基本概念
操作共享数据时需要令某个线程用代码把共享数据锁住,然后再操作数据,操作完成后进行解锁。其他想操作共享数据的线程必须等待该线程解锁之后再锁定住共享数据,继而操作数据完成后再解锁。Emmm…突然想到公共厕所是怎么回事。-_-|| 。人-线程,厕所-共享数据,锁-厕所门
互斥量(Mutex)是个类对象,可以理解成一把锁。多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标志是lock函数返回) ,如果没锁成功,那么流程就会卡在lock()这里不断去尝试这把锁头。
注意:互斥量使用要小心,保护数据不多也不少,少了,没达到保护效果;多了,影响效率。
2.互斥量的用法
2.1lock(),unlock()
使用步骤:先lock,操作共享数据,再unlock
注意:lock与unlock要成对使用。有lock必然有unlock,也不允许调用两次lock却调用一次unlock,这些非对称数量的调用都会导致代码的不稳定甚至崩溃。在if条件中,如果判断条件是共享数据,那么一定要在每个分支中都加上unlock。不加unlock会很难排查错误。毕竟程序不知何时才能运行到另一个分支,但是一旦运行到没有unlock的分支就会出现debug error。
lock(),unlock()代码示例:
(老师视频27分钟的那段代码有问题,会导致取数据的线程一直取0,为了符合我们上一节的需求,我改造了代码如下,并且为了输出规范,在输出语句上也加了锁和其他信息,简直强迫症福音):
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include<mutex>
using namespace std;
//用成员函数作为线程函数
class GameSever {
public:
//把收到的消息(玩家命令)放到一个队列的线程函数
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
//怎么可能真正收到玩家命令,所以我们循环十万个数代表接受到的命令,把这十万个数放到队列中
mymutex.lock();
cout << "\ninMsgRecvQueue()执行,插入元素为" << i << endl;
msgRecvQueue.push_back(i);//新数据放到队列尾端
mymutex.unlock();
}
}
//为了方便加锁解锁 把读取共享数据的线程加锁解锁的部分单独抽象成一个函数
int outLULProc()
{
mymutex.lock();//判断共享数据msgRecvQueue是否为空时需要加锁
if (!msgRecvQueue.empty()) //若消息队列不为空,取出队列最前的元素并返回
{
int cmd = msgRecvQueue.front();//读取队列最前端的数据
msgRecvQueue.pop_front();//移除队列最前端的数据
mymutex.unlock();//程序有两个return出口,必然需要两个unlock,从而保证每个出口都能组成一对lock/unlock
return cmd;
}
mymutex.unlock();//程序有两个return出口,必然需要两个unlock,从而保证每个出口都能组成一对lock/unlock
return -1;//若消息队列为空,返回-1
}
//把数据从消息队列取出的线程
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
int result = outLULProc();
if (result != -1) //消息队列不为空
{
mymutex.lock();//这里不需要加锁,我有强迫症就是为了输出好看点,因为这里加锁可以让输出完整,线程输出不会抢占控制台,毕竟控制台只有一个也算是“共享资源”啊 -_-||
cout << "\noutMsgRecvQueue已执行,取出一个元素" << result <<",这是第"<<i<<"次取操作"<<",此时队列中还剩下"<< msgRecvQueue.size()<<"个元素"<< endl;
//....这里考虑对读取的数据result进行处理
mymutex.unlock();//这里不需要加锁,我有强迫症就是为了输出好看点
}
else//消息队列为空
{
mymutex.lock();//这里不需要加锁,我有强迫症就是为了输出好看点
cout << "\noutMsgRecvQueue()已执行,但是目前消息队列为空未取出元素" << ",这是第" << i << "次取操作" << endl;
mymutex.unlock();//这里不需要加锁,我有强迫症就是为了输出好看点
}
//cout << "\noutMsgRecvQueue执行完毕" << endl;
}
}
private:
list<int> msgRecvQueue;//容器,代表玩家发送过来的命令
std::mutex mymutex;//创建一个互斥量
};
int main()
{
GameSever gs;
thread myInMsgObj(&GameSever::inMsgRecvQueue, ref(gs));//第二个参数是引用,才能保证线程里用的是同一对象gs,才能使用同一个消息队列
thread myOutMsgObj(&GameSever::outMsgRecvQueue, ref(gs));//第二个参数是引用,才能保证线程里用的是同一对象gs,才能使用同一个消息队列
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
输出结果展示(部分)
2.2std::lock_guard类模板
为了防止忘记unlock,引入了一个std::lock_guard的类模板,自动unlock。类似于会自动释放内存的智能指针。
lock_guard能直接取代lock与unlock兄弟
lock_guard()构造函数里执行了mutex::lock(),在析构函数里执行了mutex::unlock()。也就是在声明的时候lock,在return前一行或者声明所在大括号结束之前unlock。
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include<mutex>
using namespace std;
//用成员函数作为线程函数
class GameSever {
public:
//把收到的消息(玩家命令)放到一个队列的线程函数
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
//怎么可能真正收到玩家命令,所以我们循环十万个数代表接受到的命令,把这十万个数放到队列中
std::lock_guard<std::mutex> sbguard(mymutex);声明的时候执行拷贝构造函数mutex::lock(),声明所在大括号结束之前一行执行析构函数mutex::unlock()
cout << "\ninMsgRecvQueue()执行,插入元素为" << i << endl;
msgRecvQueue.push_back(i);//新数据放到队列尾端
}
return;
}
//为了方便加锁解锁 把读取共享数据的线程加锁解锁的部分单独抽象成一个函数
int outLULProc()
{
std::lock_guard<std::mutex> sbguard(mymutex);//声明的时候执行拷贝构造函数mutex::lock(),return前一行执行析构函数mutex::unlock()
if (!msgRecvQueue.empty()) //若消息队列不为空,取出队列最前的元素并返回
{
int cmd = msgRecvQueue.front();//读取队列最前端的数据
msgRecvQueue.pop_front();//移除队列最前端的数据
return cmd;
}
return -1;//若消息队列为空,返回-1
}
//把数据从消息队列取出的线程
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
int result = outLULProc();
if (result != -1) //消息队列不为空
{
mymutex.lock();//这里不需要加锁,我有强迫症就是为了输出好看点,因为这里加锁可以让输出完整,线程输出不会抢占控制台,毕竟控制台只有一个也算是“共享资源”啊 -_-||
cout << "\noutMsgRecvQueue已执行,取出一个元素" << result <<",这是第"<<i<<"次取操作"<<",此时队列中还剩下"<< msgRecvQueue.size()<<"个元素"<< endl;
//....这里考虑对读取的数据result进行处理
mymutex.unlock();//这里不需要加锁,我有强迫症就是为了输出好看点
}
else//消息队列为空
{
mymutex.lock();//这里不需要加锁,我有强迫症就是为了输出好看点
cout << "\noutMsgRecvQueue()已执行,但是目前消息队列为空未取出元素" << ",这是第" << i << "次取操作" << endl;
mymutex.unlock();//这里不需要加锁,我有强迫症就是为了输出好看点
}
//cout << "\noutMsgRecvQueue执行完毕" << endl;
}
}
private:
list<int> msgRecvQueue;//容器,代表玩家发送过来的命令
std::mutex mymutex;//创建一个互斥量
};
int main()
{
GameSever gs;
thread myInMsgObj(&GameSever::inMsgRecvQueue, ref(gs));//第二个参数是引用,才能保证线程里用的是同一对象gs,才能使用同一个消息队列
thread myOutMsgObj(&GameSever::outMsgRecvQueue, ref(gs));//第二个参数是引用,才能保证线程里用的是同一对象gs,才能使用同一个消息队列
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
输出和之前没有区别
3.1死锁演示
死锁产生的必要条件:至少有两把锁。
假设现在有两把锁:金锁和银锁;两个线程:A和B;
线程A执行的时候,先锁金锁,目前金锁A已经lock,接下来它要去锁银锁。然而还没等A锁上银锁,操作系统实行了上下文切换,此时轮到线程B执行。线程B捷足先登锁上了银锁,下一步要去锁金锁。但是金锁目前已经被A占有,B无法锁金锁;同时银锁被B占有,即使再发生上下文切换轮到A执行,也无法给银锁上锁。双方就这样僵持住,A和B都无法向下执行,出现了死锁。
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include<mutex>
using namespace std;
//用成员函数作为线程函数
class GameSever {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
golden_lock.lock();//死锁演示:注意顺序
//......
silver_lock.lock();//死锁演示:注意顺序
cout << "\ninMsgRecvQueue()执行,插入元素为" << i << endl;
msgRecvQueue.push_back(i);
silver_lock.unlock();//死锁演示:注意顺序
golden_lock.unlock();//死锁演示:注意顺序
}
return;
}
int outLULProc()
{
silver_lock.lock();//死锁演示:注意顺序
//......
golden_lock.lock();//死锁演示:注意顺序
if (!msgRecvQueue.empty())
{
int cmd = msgRecvQueue.front();
msgRecvQueue.pop_front();
golden_lock.unlock();//死锁演示:注意顺序,反着来才会产生死锁
//......
silver_lock.unlock();//死锁演示:注意顺序
return cmd;
}
golden_lock.unlock();//死锁演示:注意顺序
//......
silver_lock.unlock();//死锁演示:注意顺序
return -1;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
int result = outLULProc();
if (result != -1)
{
mymutex.lock();
cout << "\noutMsgRecvQueue已执行,取出一个元素" << result <<",这是第"<<i<<"次取操作"<<",此时队列中还剩下"<< msgRecvQueue.size()<<"个元素"<< endl;
//....这里考虑对读取的数据result进行处理
mymutex.unlock();
}
else//消息队列为空
{
mymutex.lock();
cout << "\noutMsgRecvQueue()已执行,但是目前消息队列为空未取出元素" << ",这是第" << i << "次取操作" << endl;
mymutex.unlock();
}
//cout << "\noutMsgRecvQueue执行完毕" << endl;
}
}
private:
list<int> msgRecvQueue;
std::mutex mymutex;
std::mutex golden_lock;//创建一个互斥量
std::mutex silver_lock;//创建另一个互斥量
};
int main()
{
GameSever gs;
thread myInMsgObj(&GameSever::inMsgRecvQueue, ref(gs));
thread myOutMsgObj(&GameSever::outMsgRecvQueue, ref(gs));
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
程序往下走了几行不动了,出现了死锁。in在等着out释放锁,out在等着in释放锁。
3.2死锁的一般解决的方案
上述问题,只要保证互斥量上锁顺序一致即可解决
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include<mutex>
using namespace std;
//用成员函数作为线程函数
class GameSever {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
golden_lock.lock();//死锁解决方案:翻转上锁顺序
//......
silver_lock.lock();//死锁解决方案:翻转上锁顺序
cout << "\ninMsgRecvQueue()执行,插入元素为" << i << endl;
msgRecvQueue.push_back(i);
silver_lock.unlock();//死锁解决方案:翻转上锁顺序
golden_lock.unlock();//死锁解决方案:翻转上锁顺序
}
return;
}
int outLULProc()
{
golden_lock.lock();//死锁解决方案:翻转上锁顺序
//......
silver_lock.lock();//死锁解决方案:翻转上锁顺序
if (!msgRecvQueue.empty())
{
int cmd = msgRecvQueue.front();
msgRecvQueue.pop_front();
silver_lock.unlock();//死锁解决方案:翻转上锁顺序
//......
golden_lock.unlock();//死锁解决方案:翻转上锁顺序
return cmd;
}
silver_lock.unlock();//死锁解决方案:翻转上锁顺序
//......
golden_lock.unlock();//死锁解决方案:翻转上锁顺序
return -1;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
int result = outLULProc();
if (result != -1)
{
mymutex.lock();
cout << "\noutMsgRecvQueue已执行,取出一个元素" << result <<",这是第"<<i<<"次取操作"<<",此时队列中还剩下"<< msgRecvQueue.size()<<"个元素"<< endl;
//....这里考虑对读取的数据result进行处理
mymutex.unlock();
}
else//消息队列为空
{
mymutex.lock();
cout << "\noutMsgRecvQueue()已执行,但是目前消息队列为空未取出元素" << ",这是第" << i << "次取操作" << endl;
mymutex.unlock();
}
//cout << "\noutMsgRecvQueue执行完毕" << endl;
}
}
private:
list<int> msgRecvQueue;
std::mutex mymutex;
std::mutex golden_lock;//创建一个互斥量
std::mutex silver_lock;//创建另一个互斥量
};
int main()
{
GameSever gs;
thread myInMsgObj(&GameSever::inMsgRecvQueue, ref(gs));
thread myOutMsgObj(&GameSever::outMsgRecvQueue, ref(gs));
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
3.3std::lock()函数模板
一次锁住两个以上的互斥量,不存在这种在多个线程中因为锁的顺序导致死锁的风险问题。(用的比较少)
std::lock()如果互斥量中有一个没锁住,它就会立即把已经锁住的解锁,等下一个时间片到来的时候再次尝试锁住所有互斥量。直到所有互斥量都锁住程序才能往下走。所以它的结果是要么两个互斥量都锁住,要么都没锁住。
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include<mutex>
using namespace std;
//用成员函数作为线程函数
class GameSever {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
std::lock(golden_lock, silver_lock);//相当于每个互斥量都调用了lock
cout << "\ninMsgRecvQueue()执行,插入元素为" << i << endl;
msgRecvQueue.push_back(i);
silver_lock.unlock();
golden_lock.unlock();
}
return;
}
int outLULProc()
{
std::lock(golden_lock, silver_lock);//相当于每个互斥量都调用了lock
if (!msgRecvQueue.empty())
{
int cmd = msgRecvQueue.front();
msgRecvQueue.pop_front();
silver_lock.unlock();
//......
golden_lock.unlock();
return cmd;
}
silver_lock.unlock();
//......
golden_lock.unlock();
return -1;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
int result = outLULProc();
if (result != -1)
{
mymutex.lock();
cout << "\noutMsgRecvQueue已执行,取出一个元素" << result <<",这是第"<<i<<"次取操作"<<",此时队列中还剩下"<< msgRecvQueue.size()<<"个元素"<< endl;
//....这里考虑对读取的数据result进行处理
mymutex.unlock();
}
else//消息队列为空
{
mymutex.lock();
cout << "\noutMsgRecvQueue()已执行,但是目前消息队列为空未取出元素" << ",这是第" << i << "次取操作" << endl;
mymutex.unlock();
}
//cout << "\noutMsgRecvQueue执行完毕" << endl;
}
}
private:
list<int> msgRecvQueue;
std::mutex mymutex;
std::mutex golden_lock;//创建一个互斥量
std::mutex silver_lock;//创建另一个互斥量
};
int main()
{
GameSever gs;
thread myInMsgObj(&GameSever::inMsgRecvQueue, ref(gs));
thread myOutMsgObj(&GameSever::outMsgRecvQueue, ref(gs));
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
3.4std::lock_guard的std::adopt_lock参数
一起用std::lock()和std::lock_guard可以兼具前者一起锁互斥量,避免死锁的优点,和后者直接在析构时调用unlock,不用再额外写unlock的优点。
但是lock_guard在声明时会自动调用lock,这就会与std::lock()冲突(相当于锁两次)。所以需要在lock_guard后面加上adopt_lock参数。
std::adopt_lock是个结构体对象,起一个标记作用。作用就是表示这个互斥量已经lock()过了,不需要在std::lock_guard 里面对对象再次进行lock了。
代码实现如下:
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include<mutex>
using namespace std;
//用成员函数作为线程函数
class GameSever {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
std::lock(golden_lock, silver_lock);//相当于每个互斥量都调用了lock
std::lock_guard<std::mutex> sbguard1(golden_lock, std::adopt_lock);//说明不要在std::lock_guard 里面对golden_lock再次进行lock了
std::lock_guard<std::mutex> sbguard2(silver_lock, std::adopt_lock);
cout << "\ninMsgRecvQueue()执行,插入元素为" << i << endl;
msgRecvQueue.push_back(i);
//silver_lock.unlock();
//golden_lock.unlock();
}
return;
}
int outLULProc()
{
std::lock(golden_lock, silver_lock);//相当于每个互斥量都调用了lock
std::lock_guard<std::mutex> sbguard1(golden_lock, std::adopt_lock);
std::lock_guard<std::mutex> sbguard2(silver_lock, std::adopt_lock);
if (!msgRecvQueue.empty())
{
int cmd = msgRecvQueue.front();
msgRecvQueue.pop_front();
//silver_lock.unlock();
//......
//golden_lock.unlock();
return cmd;
}
//silver_lock.unlock();
//......
//golden_lock.unlock();
return -1;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
int result = outLULProc();
if (result != -1)
{
mymutex.lock();
cout << "\noutMsgRecvQueue已执行,取出一个元素" << result <<",这是第"<<i<<"次取操作"<<",此时队列中还剩下"<< msgRecvQueue.size()<<"个元素"<< endl;
//....这里考虑对读取的数据result进行处理
mymutex.unlock();
}
else//消息队列为空
{
mymutex.lock();
cout << "\noutMsgRecvQueue()已执行,但是目前消息队列为空未取出元素" << ",这是第" << i << "次取操作" << endl;
mymutex.unlock();
}
//cout << "\noutMsgRecvQueue执行完毕" << endl;
}
}
private:
list<int> msgRecvQueue;
std::mutex mymutex;
std::mutex golden_lock;//创建一个互斥量
std::mutex silver_lock;//创建另一个互斥量
};
int main()
{
GameSever gs;
thread myInMsgObj(&GameSever::inMsgRecvQueue, ref(gs));
thread myOutMsgObj(&GameSever::outMsgRecvQueue, ref(gs));
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}