5 互斥量概念、用法、死锁演示及解决详解
文章目录
5.1 互斥量(mutex)的基本概念
保护共享数据,操作时,用代码把共享数据锁住,操作数据,解锁,数据被锁时,其他像操作共享数据必须等待解锁。
5.2 互斥量的用法
互斥量是个类对象。理解成一把锁,多个线程尝试用lock()
成员函数来加锁头,只有一个线程能够锁成功(成功的标志是lock()
函数返回),如果没锁成功,那么流程卡在lock()这里不断的尝试去锁这把锁头。
互斥量使用要小心,保护数据不多也不少,少了,没达到保护效果,多了,影响效率。
5.2.1 lock(), unlock()
lock()
和unlock()
要成对使用,有lock()
必然有unlock()
,每调用一次lock()
,必然应该调用一次unlock()
。
代码5-2-1
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
// 把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
cout << "inMsgRecvQueue执行,插入一个元素" << i << endl;
m_mutex.lock();
msgRecvQueue.push_back(i); // 假设i就是收到玩家的命令,直接弄到消息队列里面
m_mutex.unlock();
}
}
// 把数据从消息队列中取出的线程;
void outMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
m_mutex.lock();
if (!this->msgRecvQueue.empty()) {
int command = this->msgRecvQueue.front();
this->msgRecvQueue.pop_front();
cout << "outMsgRecvQueue执行,命令为:" << command << endl;
} else {
cout << "outMsgRecvQueue执行,消息队列为空" << endl;
}
m_mutex.unlock();
}
}
private:
list<int> msgRecvQueue; // 容器,专门用于代表玩家给咱们发送过来的命令。
mutex m_mutex;
};
int main() {
A a;
thread out_t(&A::outMsgRecvQueue, &a); // 第二个参数是 引用, 才能保证线程里,用的是同一个对象
thread in_t(&A::inMsgRecvQueue, &a);
out_t.join();
in_t.join();
return 0;
}
运行结果
...
inMsgRecvQueue执行,插入一个元素37
inMsgRecvQueue执行,插入一个元素38
inMsgRecvQueue执行,插入一个元素39
inMsgRecvQueue执行,插入一个元素40
inMsgRecvQueue执行,插入一个元素41
outMsgRecvQueue执行,命令为:1
inMsgRecvQueue执行,插入一个元素42
inMsgRecvQueue执行,插入一个元素43
inMsgRecvQueue执行,插入一个元素44
...
5.2.2 std::lock_guard类模版
为了防止编程当中忘记unlock()
,引入一个叫std::lock_guard
的类模版:你忘记unlock
不要紧,我替你unlock
。
原理:在lock_guard
类中,构造函数有lock()
,析构函数中有unlock()
,对象生命周期结束时,所以会自动调用unlock()
。
代码5-2-2
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
// 把收到的消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
cout << "inMsgRecvQueue执行,插入一个元素" << i << endl;
m_mutex.lock();
msgRecvQueue.push_back(i); // 假设i就是收到玩家的命令,直接弄到消息队列里面
m_mutex.unlock();
}
}
// 把数据从消息队列中取出的线程;
bool outMsgHelper() {
lock_guard<mutex> sbguard(m_mutex);
if (!msgRecvQueue.empty()) {
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
cout << "outMsgRecvQueue执行,命令为:" << command << endl;
return true;
}
cout << "outMsgRecvQueue执行,消息队列为空" << endl;
return false;
}
void outMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
outMsgHelper();
}
}
private:
list<int> msgRecvQueue; // 容器,专门用于代表玩家给咱们发送过来的命令。
mutex m_mutex;
};
int main() {
A a;
thread out_t(&A::outMsgRecvQueue, &a); // 第二个参数是 引用, 才能保证线程里,用的是同一个对象
thread in_t(&A::inMsgRecvQueue, &a);
out_t.join();
in_t.join();
return 0;
}
运行结果
...
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空inMsgRecvQueue执行,插入一个元素
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
...
为了增加lock_guard
的灵活性,可以用花括号来限制lock_guard
对象的生命周期
代码5-2-3
bool outMsgHelper() {
{
...处理其他逻辑
lock_guard<mutex> sbguard(m_mutex);
if (!msgRecvQueue.empty()) {
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
cout << "outMsgRecvQueue执行,命令为:" << command << endl;
return true;
}
cout << "outMsgRecvQueue执行,消息队列为空" << endl;
return false;
}
...处理其他逻辑
}
运行结果
略
5.3 死锁
比如我有两把锁(死锁这个问题,是由至少两个所有也就是两个互斥量才能产生),金锁(GoldLock
),银锁(SilverLock
),两个线程A,B,进行以下过程:
- 线程A执行的时候,这个线程先锁
GoldLock
,把GoldLock
锁成功后,然后去锁SilverLock
。 - 出现了上下文切换。
- 线程B执行了,这个线程先锁
SilverLock
,因为银锁还没有被锁,所以银锁会lock成功,然后去锁GoldLock
。 - 此时此刻,因为A锁不了
SilverLock
,B锁不了GoldLock
,A和B的流程都走不下去,A和B产生了死锁。
5.3.1 死锁的演示
代码5-3-1
A和B锁的次数要设置得比较多(即代码中的i
变量),不然观察不到互锁的现象,并不是不会发生,是发生的概率太小了。
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class E {
public:
void A() {
int i = 1000;
while(i--) {
GoldLock.lock();
SilverLock.lock();
m_name = "A";
cout << m_name << endl;
// 下面的顺序无所谓
SilverLock.unlock();
GoldLock.unlock();
}
}
void B() {
int i = 1000;
while(i--) {
SilverLock.lock();
GoldLock.lock();
m_name = "B";
cout << m_name << endl;
GoldLock.unlock();
SilverLock.unlock();
}
}
private:
string m_name;
mutex GoldLock, SilverLock;
};
int main() {
E e;
thread t_a(&E::A, &e);
thread t_b(&E::B, &e);
t_a.join();
t_b.join();
return 0;
}
运行结果
...
A
A
A
A
A
A
A
A
执行到最后一行便停止了,产生了死锁。
5.3.2 死锁的一般解决方案
破坏死锁的必要条件之循环等待:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。在代码5-3-1
修改为如下即可:
void A() {
int i = 1000;
while(i--) {
GoldLock.lock();
SilverLock.lock();
m_name = "A";
cout << m_name << endl;
SilverLock.unlock();
GoldLock.unlock();
}
}
void B() {
int i = 1000;
while(i--) {
// 交换了顺序
GoldLock.lock();
SilverLock.lock();
m_name = "B";
cout << m_name << endl;
// 下面的顺序无所谓
GoldLock.unlock();
SilverLock.unlock();
}
}
5.3.3 std::lock()函数模版
能力:一次锁住多个锁头(至少两个,多了不限),在多线程中,它不会因资源的请求顺序而产生死锁。如果有一个互斥量没锁住,它就释放已经请求到的所有资源,过一段时间,再以相同的方式去请求。
代码5-3-2
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class E {
public:
void A() {
int i = 1000;
while(i--) {
lock(GoldLock, SilverLock);
m_name = "A";
cout << m_name << endl;
SilverLock.unlock();
GoldLock.unlock();
}
}
void B() {
int i = 1000;
while(i--) {
lock(GoldLock, SilverLock);
m_name = "B";
cout << m_name << endl;
GoldLock.unlock();
SilverLock.unlock();
}
}
private:
string m_name;
mutex GoldLock, SilverLock;
};
int main() {
E e;
thread t_a(&E::A, &e);
thread t_b(&E::B, &e);
t_a.join();
t_b.join();
return 0;
}
运行结果
...
A
A
A
A
A
A
B
B
...
5.3.4 std::lock_guard的std::adopt_lock参数
因为用std::lock()
解决了死锁问题,但是我们还需要关心unlock()
,要全部解锁完毕,那怎么操作才能自动解锁呢?之前我们用到了std::lock_guard()
,刚好这个可以解决我们的问题~
代码5-3-4
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class E {
public:
void A() {
int i = 10000;
while(i--) {
lock(GoldLock, SilverLock);
lock_guard<mutex> guardGoldLock(GoldLock, adopt_lock);
lock_guard<mutex> guardSilverLock(SilverLock, adopt_lock);
m_name = "A";
cout << m_name << endl;
}
}
void B() {
int i = 10000;
while(i--) {
lock(GoldLock, SilverLock);
lock_guard<mutex> guardGoldLock(GoldLock, adopt_lock);
lock_guard<mutex> guardSilverLock(SilverLock, adopt_lock);
m_name = "B";
cout << m_name << endl;
}
}
private:
string m_name;
mutex GoldLock, SilverLock;
};
int main() {
E e;
thread t_a(&E::A, &e);
thread t_b(&E::B, &e);
t_a.join();
t_b.join();
return 0;
}
运行结果
...
A
A
A
A
B
B
B
B
B
B
...