一碌廓、前言
本文接上文 【學習筆記】C++并發(fā)與多線程筆記三:數(shù)據(jù)共享 的內(nèi)容传轰,主要包含互斥量的基本概念、用法谷婆、死鎖演示以及解決方案慨蛙。
二、互斥量的基本概念
互斥量就是個類對象波材,可以理解成一把鎖股淡,多個線程嘗試用lock()成員函數(shù)來加鎖,只有一個線程能鎖定成功廷区,如果沒有鎖成功唯灵,那么流程將卡在lock()這里不斷嘗試去鎖定。
備注:互斥量使用要小心隙轻,上鎖的代碼需要根據(jù)實際情況考慮(只保護需要保護的數(shù)據(jù))埠帕,少了達不到效果,多了影響效率玖绿。
三敛瓷、互斥量的用法
首先,需要包含頭文件#include <mutex>
斑匪,然后使用 mutex 類即可創(chuàng)建鎖對象:
#include <iostream>
#include <mutex>
using namespace std;
class A
{
private:
mutex my_mutex; /* 創(chuàng)建一個互斥鎖 */
};
3.1 lock()和unlock()
在代碼中 lock()
(上鎖)和 unlock()
(解鎖)必須成對使用呐籽,步驟如下:
- 先
lock()
上鎖; - 然后操作共享數(shù)據(jù)蚀瘸;
- 再
unlock()
解鎖
備注:代碼中使用互斥量的時絕不允許非對稱調(diào)用狡蝶,即
lock()
和unlock()
一定是成對出現(xiàn)的。
以上篇文章的示例代碼為例贮勃,我們需要保護的共享數(shù)據(jù)為消息隊列 msgRecvQueue
贪惹,在讀寫這個隊列前就需要上鎖,讀寫完畢后需要解鎖寂嘉,代碼如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
public:
/* 把收到的消息(玩家命令)存到隊列中 */
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
my_mutex.lock(); /* 上鎖 */
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
my_mutex.unlock(); /* 解鎖 */
}
}
/* 消息隊列不為空時奏瞬,返回并彈出第一個元素 */
bool outMsgLULProc(int &command)
{
my_mutex.lock(); /* 上鎖 */
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); /* 返回第一個元素 */
msgRecvQueue.pop_front(); /* 移除第一個元素 */
my_mutex.unlock(); /* 解鎖(每個分支都需要解鎖,別漏了) */
return true;
}
my_mutex.unlock(); /* 解鎖(每個分支都需要解鎖泉孩,別漏了) */
return false;
}
/* 把數(shù)據(jù)從消息隊列中取出 */
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result)
cout << "outMsgLULProc exec, and pop_front: " << command << endl;
else
cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
cout << "outMsgRecvQueue exec end!" << i << endl;
}
}
private:
list<int> msgRecvQueue; /* 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 */
mutex my_mutex; /* 創(chuàng)建一個互斥鎖 */
};
int main()
{
A obj;
thread myInMsgObj(&A::inMsgRecvQueue, &obj);
thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
myInMsgObj.join();
myOutMsgObj.join();
cout << "Hello World!" << endl;
return 0;
}
3.2 std::lock_guard類模板
我們在代碼中上鎖后硼端,一定要記得解鎖,如果忘記解鎖會導致程序運行異常寓搬,而且通常很難排查显蝌。為了防止開發(fā)者忘記解鎖,C++11引入了一個叫做 std::lock_guard
的類模板,它在開發(fā)者忘記解鎖的時候曼尊,會替開發(fā)者自動解鎖酬诀。
備注:
std::lock_guard
可以直接取代lock()
和unlock()
,也就說使用std::lock_guard
后骆撇,就不能再使用lock()
和unlock()
了瞒御。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
public:
/* 把收到的消息(玩家命令)存到隊列中 */
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
lock_guard<mutex> m_guard(my_mutex);
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
}
}
/* 消息隊列不為空時,返回并彈出第一個元素 */
bool outMsgLULProc(int &command)
{
/**
* m_guard 是一個 lock_guard 對象神郊。
* lock_guard 構(gòu)造函數(shù)里執(zhí)行了 lock()肴裙。
* lock_guard 析構(gòu)函數(shù)里執(zhí)行了 unlock()。
*/
lock_guard<mutex> m_guard(my_mutex);
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); /* 返回第一個元素 */
msgRecvQueue.pop_front(); /* 移除第一個元素 */
return true;
}
return false;
}
/* 把數(shù)據(jù)從消息隊列中取出 */
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result)
cout << "outMsgLULProc exec, and pop_front: " << command << endl;
else
cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
cout << "outMsgRecvQueue exec end!" << i << endl;
}
}
private:
list<int> msgRecvQueue; /* 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 */
mutex my_mutex; /* 創(chuàng)建一個互斥鎖 */
};
int main()
{
A obj;
thread myInMsgObj(&A::inMsgRecvQueue, &obj);
thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
myInMsgObj.join();
myOutMsgObj.join();
cout << "Hello World!" << endl;
return 0;
}
std::lock_guard
雖然用起來方便涌乳,但是不夠靈活蜻懦,它只能在析構(gòu)函數(shù)中 unlock()
,也就是對象被釋放的時候夕晓,這通常是在函數(shù)返回的時候宛乃,或者通過添加代碼塊 { /* 代碼塊 */ }
限定作用域來指定釋放時機。
四蒸辆、死鎖
一個簡單的例子:
- 張三在北京說:等李四來了之后征炼,我就去廣東。
- 李四在廣東說:等張三來了之后躬贡,我就去北京谆奥。
這兩個人一直等待對方就形成了死鎖。
同理拂玻,假如在代碼中有兩把鎖(至少有兩個互斥量存在才會產(chǎn)生死鎖)分別稱為鎖1酸些、鎖2,并且有兩個線程分別稱為線程A和線程B檐蚜。只有在某個線程同時獲得鎖1和鎖2時魄懂,才能完成某項工作:
- 線程A執(zhí)行時,先上鎖1熬甚,再上鎖2逢渔。
- 線程B執(zhí)行時肋坚,先上鎖2乡括,再上鎖1。
若在程序執(zhí)行線程A的過程中智厌,上好了鎖1后诲泌,出現(xiàn)了上下文切換,系統(tǒng)調(diào)度轉(zhuǎn)去執(zhí)行線程B铣鹏,把鎖2給上了敷扫,那么后續(xù)線程A拿不到鎖2,線程B拿不到鎖1,兩條線程都沒法往下執(zhí)行葵第,即出現(xiàn)了死鎖绘迁。
4.1 死鎖演示
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
public:
/* 把收到的消息(玩家命令)存到隊列中 */
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
m_mutex1.lock(); /* 實際代碼中,兩把鎖不一定同時上卒密,它們可能保護不同的數(shù)據(jù) */
m_mutex2.lock();
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
m_mutex2.unlock();
m_mutex1.unlock();
}
}
/* 消息隊列不為空時缀台,返回并彈出第一個元素 */
bool outMsgLULProc(int &command)
{
m_mutex2.lock();
m_mutex1.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); /* 返回第一個元素 */
msgRecvQueue.pop_front(); /* 移除第一個元素 */
m_mutex1.unlock();
m_mutex2.unlock();
return true;
}
m_mutex1.unlock();
m_mutex2.unlock();
return false;
}
/* 把數(shù)據(jù)從消息隊列中取出 */
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result)
cout << "outMsgLULProc exec, and pop_front: " << command << endl;
else
cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
cout << "outMsgRecvQueue exec end!" << i << endl;
}
}
private:
list<int> msgRecvQueue; /* 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 */
mutex m_mutex1; /* 創(chuàng)建互斥量1 */
mutex m_mutex2; /* 創(chuàng)建互斥量2 */
};
int main()
{
A obj;
thread myInMsgObj(&A::inMsgRecvQueue, &obj);
thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
myInMsgObj.join();
myOutMsgObj.join();
cout << "Hello World!" << endl;
return 0;
}
4.2 死鎖的一般解決方案
通常來講,只要保證多個互斥量上鎖的順序一致哮奇,就不會出現(xiàn)死鎖膛腐,比如把上面示例代碼的兩個線程回調(diào)函數(shù)中的上鎖順序改一下,保持一致就好了(都改為先上鎖1鼎俘,再上鎖2)哲身。
4.3 std::lock()函數(shù)模板
std::lock()
函數(shù)模板是C++11引入的,它能一次鎖住兩個或兩個以上的互斥量贸伐,并且它不存在上述的在多線程中由于上鎖順序問題造成的死鎖現(xiàn)象勘天,原因如下:
std::lock()
函數(shù)模板在鎖定兩個互斥量時,只有兩種情況:
- 兩個互斥量都沒有鎖坠髫ぁ误辑;
- 兩個互斥量都被鎖住。
如果只鎖了一個歌逢,另一個沒鎖成功巾钉,則它會立即把已經(jīng)鎖住的互斥量解鎖。
/* 把收到的消息(玩家命令)存到隊列中 */
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
std::lock(m_mutex1, m_mutex2);
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
m_mutex2.unlock(); /* 這里別忘記解鎖 */
m_mutex1.unlock(); /* 這里別忘記解鎖 */
}
}
4.4 std::lock_guard的std::adopt_lock參數(shù)
在使用 std::lock()
函數(shù)模板鎖上多個互斥量時秘案,也必須得記得把每個互斥量解鎖砰苍,此時借助 std::lock_guard
的 std::adopt_lock
參數(shù)可以省略解鎖的代碼。
/* 把收到的消息(玩家命令)存到隊列中 */
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
std::lock(m_mutex1, m_mutex2); /* 鎖上兩個互斥量 */
std::lock_guard<std::mutex> m_guard1(m_mutex1, std::adopt_lock); /* 構(gòu)造時不上鎖阱高,但析構(gòu)時解鎖 */
std::lock_guard<std::mutex> m_guard2(m_mutex2, std::adopt_lock); /* 構(gòu)造時不上鎖赚导,但析構(gòu)時解鎖 */
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
}
}