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
...