一瞪慧、前言
本文接上文 【學(xué)習(xí)筆記】C++并發(fā)與多線程筆記四:互斥量(概念搔谴、用法毅戈、死鎖) 的內(nèi)容觉渴,主要紀(jì)錄 unique_lock
的使用方法以及原理夯秃。
二座咆、uniqie_lock取代lock_quard
uniqie_lock 是個(gè)類模板,它的功能跟 lock_quard 類似仓洼,但比 lock_quard 更靈活介陶。在工作中,一般用 lock_quard (推薦使用)就足夠了色建,但在一些特殊的場景下會(huì)用到 uniqie_lock哺呜。
在上篇文章中講到了 lock_quard 取代了 mutex 的 lock() 和 unlock(),在 lock_quard 的構(gòu)造函數(shù)中上鎖箕戳,在析構(gòu)函數(shù)中解鎖某残,這點(diǎn)其實(shí)在 uniqie_lock 中也是一樣的。
uniqie_lock 在使用上比 lock_quard 靈活陵吸,但代價(jià)就是效率會(huì)低一點(diǎn)玻墅,并且內(nèi)存占用量也會(huì)相對高一些。
uniqie_lock 的缺省用法實(shí)際上與 lock_quard 一樣壮虫,可以直接替換澳厢,代碼如下:
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A {
public:
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
std::unique_lock<std::mutex> m_guard1(m_mutex1);
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
}
}
/* 消息隊(duì)列不為空時(shí),返回并彈出第一個(gè)元素 */
bool outMsgLULProc(int& command) {
std::unique_lock<std::mutex> m_guard1(m_mutex1);
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front(); /* 返回第一個(gè)元素 */
msgRecvQueue.pop_front(); /* 移除第一個(gè)元素 */
return true;
}
return false;
}
/* 把數(shù)據(jù)從消息隊(duì)列中取出 */
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; /* 容器(實(shí)際上是雙向鏈表):存放玩家發(fā)生命令的隊(duì)列 */
mutex m_mutex1; /* 創(chuàng)建互斥量1 */
};
int main() {
A obj;
thread myInMsgObj(&A::inMsgRecvQueue, &obj);
thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
myInMsgObj.join();
myOutMsgObj.join();
cout << "Hello World!" << endl;
return 0;
}
三、uniqie_lock的第二個(gè)參數(shù)
uniqie_lock 的第二個(gè)參數(shù)是一個(gè)標(biāo)志位剩拢,其可取參數(shù)詳見下文线得。
3.1 std::adopt_lock
該標(biāo)記表示這個(gè)互斥鎖已經(jīng)被 lock() 了,uniqie_lock 不會(huì)再重復(fù)上鎖裸扶。
也就是說該標(biāo)記的效果是:假設(shè)調(diào)用方線程已經(jīng)擁有了互斥鎖的所有權(quán)框都,通知 uniqie_lock 不需要再構(gòu)造函數(shù)中 lock 這個(gè)互斥鎖了。
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
m_mutex1.lock(); /* 先lock()呵晨,才能在 unique_lock 中用 adopt_lock 標(biāo)準(zhǔn) */
std::unique_lock<std::mutex> m_guard1(m_mutex1, std::adopt_lock);
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
}
}
備注:lock_quard 中該標(biāo)記的含義相同魏保。
3.2 std::try_to_lock
假設(shè)我們在 myOutMsgObj 線程的回調(diào)函數(shù)中拿到互斥鎖后,sleep 20 秒摸屠,就存在一個(gè)問題谓罗,myOutMsgObj 線程占用了互斥鎖資源,卻不向下執(zhí)行季二,導(dǎo)致另一條線程 myInMsgObj 一直都沒辦法拿到互斥鎖檩咱,也要等 20 秒,造成計(jì)算資源浪費(fèi)胯舷。
/* 消息隊(duì)列不為空時(shí)刻蚯,返回并彈出第一個(gè)元素 */
bool outMsgLULProc(int& command) {
std::unique_lock<std::mutex> m_guard1(m_mutex1);
std::chrono::milliseconds dura(20000); /* 20秒 */
std::this_thread::sleep_for(dura); /* sleep 20秒 */
if (!msgRecvQueue.empty()) {
command = msgRecvQueue.front(); /* 返回第一個(gè)元素 */
msgRecvQueue.pop_front(); /* 移除第一個(gè)元素 */
return true;
}
return false;
}
為解決這個(gè)問題,uniqie_lock 引入了 try_to_lock 參數(shù)桑嘶,它表示代碼會(huì)嘗試上鎖炊汹,即使沒有成功,也會(huì)立即返回逃顶,不會(huì)阻塞的等待讨便。
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
/* 嘗試上鎖 */
std::unique_lock<std::mutex> m_guard1(m_mutex1, std::try_to_lock);
if (m_guard1.owns_lock()) { /* 如果拿到了鎖 */
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
} else {
cout << "try_to_lock fail, do something else!!!" << endl;
}
}
}
備注:使用 try_to_lock 參數(shù)前,線程中不能調(diào)用 lock()以政,否則會(huì)造成死鎖霸褒。
3.3 std::defer_lock
該標(biāo)記表示并沒有給 mutex 加鎖,即初始化了一個(gè)沒有加鎖的 mutex盈蛮。使用該標(biāo)記初始化的 uniqie_lock 對象可以靈活的調(diào)用 uniqie_lock 的成員函數(shù)废菱,這點(diǎn)將在下文中一起演示。
四抖誉、uniqie_lock的成員函數(shù)
4.1 lock()
使用 std::defer_lock
參數(shù)初始化 uniqie_lock 對象可以調(diào)用unique_lock的成員函數(shù)上鎖殊轴,并且無需在代碼中解鎖,它會(huì)自動(dòng)解鎖寸五,有點(diǎn)類似智能指針梳凛。
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
/* 初始化了沒有加鎖的m_mutex1 */
std::unique_lock<std::mutex> m_guard1(m_mutex1, std::defer_lock);
m_guard1.lock(); /* 調(diào)用unique_lock的成員函數(shù)上鎖耿币,并且無需在代碼中解鎖梳杏,它會(huì)自動(dòng)解鎖 */
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
}
}
4.2 unlock()
根據(jù) 4.1 的代碼,我們可以知道 unique_lock 的成員函數(shù) lock() 上鎖后,在對象析構(gòu)的時(shí)候會(huì)自動(dòng)解鎖十性,那為什么 unique_lock 還需要提供 unlock() 函數(shù)呢叛溢?
這里就體現(xiàn)了 unique_lock 的靈活性,它既具備智能指針自動(dòng)銷毀的特性劲适,又可以在代碼中手動(dòng)解鎖楷掉,方便程序在一段代碼中對其中某幾個(gè)部分上鎖的需求,將一些非共享代碼從鎖中提取出來霞势,從而提高效率烹植。
備注:通常,在代碼中愕贡,上鎖的代碼越少(粒度越胁莸瘛),效率越高固以。
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
/* 初始化了沒有加鎖的m_mutex1 */
std::unique_lock<std::mutex> m_guard1(m_mutex1, std::defer_lock);
m_guard1.lock(); /* 調(diào)用unique_lock的成員函數(shù)上鎖墩虹,并且無需在代碼中解鎖,它會(huì)自動(dòng)解鎖 */
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
m_guard1.unlock();
/* 非共享代碼...... */
m_guard1.lock();
/* 共享代碼...... */
m_guard1.unlock(); /* 這個(gè)unlock() 可調(diào)可不調(diào)憨琳,m_guard1 對象析構(gòu)時(shí)會(huì)自動(dòng)解鎖 */
}
}
4.3 try_lock()
這個(gè)成員函數(shù)與 std::try_to_lock
參數(shù)類似诫钓,嘗試上鎖,如果拿到鎖了篙螟,則返回 true菌湃,否則返回 false。
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
/* 初始化了沒有加鎖的m_mutex1 */
std::unique_lock<std::mutex> m_guard1(m_mutex1, std::defer_lock);
if (m_guard1.try_lock()) { /* 如果拿到鎖了 */
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
} else {
cout << "try_to_lock fail, do something else!!!" << endl;
}
}
}
4.4 release()
該成員函數(shù)的作用是返回它所管理的 mutex 對象指針闲擦,并釋放所有權(quán)慢味;也就是說,這個(gè) unique_lock 和 mutex 不再有關(guān)系墅冷。
調(diào)用以下代碼創(chuàng)建 unique_lock 類型的對象 m_guard1 時(shí)纯路,實(shí)際上是把 m_guard1 與 m_mutex1 對象綁定在一起了,release() 函數(shù)可以將這兩個(gè)對象解綁寞忿,并返回綁定的 mutex 指針驰唬,即此處的 m_mutex1 。注意這里是解綁腔彰,并不是銷毀叫编。
std::unique_lock<std::mutex> m_guard1(m_mutex1);
調(diào)用 release() 函數(shù)解綁后,我們必須保存返回的 mutex 指針霹抛,并在接下來的代碼中自行管理搓逾。
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
/* 初始化了沒有加鎖的m_mutex1 */
std::unique_lock<std::mutex> m_guard1(m_mutex1);
std::mutex* pmutex = m_guard1.release(); /* 解綁后返回之前綁定的m_mutex1,接下來我們要自行管理m_mutex1 */
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
pmutex->unlock(); /* 解綁后需要自己解鎖 */
}
}
五杯拐、uniqie_lock所有權(quán)的傳遞
當(dāng) uniqie_lock 與 mutex 對象綁定在一起才是一個(gè)完整的霞篡、能發(fā)揮作用的 uniqie_lock 實(shí)例世蔗,也就是說 uniqie_lock 需要和 mutex 配合使用,可以理解為 uniqie_lock 需要管理一個(gè) mutex朗兵。
一個(gè) mutex 對象只能被一個(gè) uniqie_lock 對象所有(擁有)污淋,即同一個(gè) mutex 無法被兩個(gè) uniqie_lock 對象同時(shí)使用,這就是 uniqie_lock 的所有權(quán)概念余掖。
/* 上鎖兩次寸爆,造成死鎖 */
std::unique_lock<std::mutex> m_guard1(m_mutex1);
std::unique_lock<std::mutex> m_guard2(m_mutex1); /* 復(fù)制所有權(quán)(程序崩潰) */
m_guard1 擁有 m_mutex1 的所有權(quán),并且 m_guard1 可以把自己 mutex(m_mutex1) 的所有權(quán)轉(zhuǎn)移給其他的 uniqie_lock 對象盐欺。
備注:所有權(quán)可以轉(zhuǎn)移赁豆,但不能復(fù)制,這與智能指針 unique_ptr 類似冗美,智能指針指向的對象同樣也是可以移動(dòng)歌憨,但不能復(fù)制。
將 m_guard1 的所有權(quán)轉(zhuǎn)移給 m_guard2墩衙,代碼如下:
std::unique_lock<std::mutex> m_guard1(m_mutex1);
/* 移動(dòng)語義务嫡,相當(dāng)于m_guard1與m_mutex1解綁;m_guard2與m_mutex1綁定 */
std::unique_lock<std::mutex> m_guard2(std::move(m_guard1));
轉(zhuǎn)移unique_lock所有權(quán)的擴(kuò)展方法:
std::unique_lock<std::mutex> rtn_unique_lock() {
std::unique_lock<std::mutex> temp_guard(m_mutex1);
/* 從函數(shù)返回一個(gè)局部的temp_guard(移動(dòng)構(gòu)造函數(shù))*/
/* 返回局部對象temp_guard會(huì)讓系統(tǒng)創(chuàng)建臨時(shí)的unique_lock對象漆改,并調(diào)用unique_lock的移動(dòng)構(gòu)造函數(shù) */
return temp_guard;
}
/* 把收到的消息(玩家命令)存到隊(duì)列中 */
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
/* rtn_unique_lock 函數(shù)中 temp_guard 對象的所有權(quán)轉(zhuǎn)移到 m_guard1 中了 */
std::unique_lock<std::mutex> m_guard1 = rtn_unique_lock();
msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
}
}