(C++11/14/17學習筆記):互斥量概念、用法、死鎖演示及解決詳解
阿新 • • 發佈:2020-12-10
技術標籤:C++11/14/17
目錄
互斥量概念、用法、死鎖演示及解決詳解
互斥量(mutex)的基本概念
- 保護共享大資料,操作時,某個執行緒 用程式碼把共享資料鎖住、操作資料、解鎖,其他想操作共享資料的執行緒必須等待解鎖,鎖定住,操作,解鎖。
- 互斥量是個類物件。
- 理解成一把鎖,多個執行緒嘗試用lock()
成員函式來加鎖這把鎖頭,只有一個執行緒能鎖定成功(成功的標誌是lock()函式返回)。- 如果沒鎖成功,那麼流程阻塞在lock()這裡不斷的嘗試去鎖這把鎖頭。
- 互斥量使用要小心,保護資料不多也不少,少了,沒達到保護效果,多了,影響效率。
互斥量的用法
- 引入標頭檔案 #include <mutex>;
lock(), unlock()
- 步驟:先lock(), 操作共享資料,再unlock()。
- lock()和unlock()要成對使用,有lock必然要有unlock,每呼叫一次lock(),必然應該呼叫一次unlock()。
- 不應該也不允許呼叫1次lock()卻呼叫了2次unlock(),也不允許呼叫2次lock卻呼叫1次unlock(),這些非對稱。
- 數量的呼叫都會導致程式碼不穩定甚至崩潰。有lock,忘記unlock的問題,非常難排查。
- 如果lock了,注意退出的地方(如 return)是不是加上了unlock,幾個出口幾個unlock。
#include <iostream> #include <string> #include <thread> #include <vector> #include <list> #include <mutex> using namespace std; class A{ public: //把收到的訊息(玩家命令)加入到一個佇列的執行緒 void inMsgRecvQueue() { for (int i = 1; i < 10000; ++i) { cout << "inMsgRecvQueue執行了,插入一個元素" << i << endl; my_mutex.lock(); msgRecvQueue.push_back(i); //假設這個數字就是玩家發來的命令,加入到訊息佇列中 my_mutex.unlock(); } } //在這個函式中加鎖 bool outMsgMutPro(int& command ) { my_mutex.lock(); if (!msgRecvQueue.empty()) { //訊息佇列不為空 command = msgRecvQueue.front(); //返回第一個元素,但不檢查元素是否存在 msgRecvQueue.pop_front(); //移除第一個元素,但不返回 my_mutex.unlock(); return true; } my_mutex.unlock(); return false; } //把訊息從訊息佇列中取出的執行緒 void outMsgRecvQueue() { int command{}; for (int i = 1; i < 10000; ++i) { bool ret = outMsgMutPro(command); if (ret) { cout << "outMsgMutPro執行了,取出一個元素" << command << endl; //這裡就針對具體的命令具體處理 //... } else { //訊息佇列為空 cout << "outMsgRecvQueue執行了,但是當前訊息佇列為空" << i << endl; } } cout << "outMsgRecvQueue()執行完畢" << endl; } private: std::list<int> msgRecvQueue; //容器(訊息佇列),專門代表玩家給我們發來的命令 std::mutex my_mutex; }; int main() { A obja; std::thread outMsgThread(&A::outMsgRecvQueue, &obja); //第二個引數是引用,保證執行緒裡操作同一個物件 std::thread inMsgThread(&A::inMsgRecvQueue, &obja); inMsgThread.join(); outMsgThread.join(); //主執行緒執行 std::cout << "主執行緒結束" << std::endl; return 0; }
std::lock_guard類模板
- 為了防止大家忘記unlock(),引入了一個叫std::lock_guard的類模板:你忘記unlock不要緊,我替你unlock()。
- 聯想智慧指標(unique_ptr<>) : 你忘記釋放記憶體不要緊,我給你釋放。
- std::lock_guard 類模板:直接取代lock() 和unlock();也就是說, 你用了lock_guard之後,再不能使用lock()和unlock()了。
- lock_guard建構函式裡執行了mutex::lock()。
- lock_guard解構函式裡執行了mutex::unlock()。
- 結合 {} ,可以控制作用的範圍(RAII)。
#include <iostream> #include <string> #include <thread> #include <vector> #include <list> #include <mutex> using namespace std; class A{ public: //把收到的訊息(玩家命令)加入到一個佇列的執行緒 void inMsgRecvQueue() { for (int i = 1; i < 10000; ++i) { cout << "inMsgRecvQueue執行了,插入一個元素" << i << endl; { std::lock_guard<mutex> mutex_guard_in(my_mutex); msgRecvQueue.push_back(i); //假設這個數字就是玩家發來的命令,加入到訊息佇列中 } //其他程式碼... } } //在這個函式中加鎖 bool outMsgMutPro(int& command ) { std::lock_guard<mutex> mutex_guard_out(my_mutex); if (!msgRecvQueue.empty()) { //訊息佇列不為空 command = msgRecvQueue.front(); //返回第一個元素,但不檢查元素是否存在 msgRecvQueue.pop_front(); //移除第一個元素,但不返回 return true; } return false; } //把訊息從訊息佇列中取出的執行緒 void outMsgRecvQueue() { int command{}; for (int i = 1; i < 10000; ++i) { bool ret = outMsgMutPro(command); if (ret) { cout << "outMsgMutPro執行了,取出一個元素" << command << endl; //這裡就針對具體的命令具體處理 //... } else { //訊息佇列為空 cout << "outMsgRecvQueue執行了,但是當前訊息佇列為空" << i << endl; } } cout << "outMsgRecvQueue()執行完畢" << endl; } private: std::list<int> msgRecvQueue; //容器(訊息佇列),專門代表玩家給我們發來的命令 std::mutex my_mutex; }; int main() { A obja; std::thread outMsgThread(&A::outMsgRecvQueue, &obja); //第二個引數是引用,保證執行緒裡操作同一個物件 std::thread inMsgThread(&A::inMsgRecvQueue, &obja); inMsgThread.join(); outMsgThread.join(); //主執行緒執行 std::cout << "主執行緒結束" << std::endl; return 0; }
死鎖
- 比如我有兩把鎖(死鎖這個問題 是由至少兩個鎖頭也就是兩個互斥量才能產生):金鎖(jinlock) 銀鎖(yinlock)。
- 兩個執行緒 A,B
- (1) 執行緒A執行的時候,這個執行緒先鎖金鎖,把金鎖lock()成功了,然後它去lock銀鎖。
- 出現了上下文切換
- (2) 執行緒B執行了,這個執行緒先鎖銀鎖,因為銀鎖還沒有被鎖,所以銀鎖會lock()成功,執行緒B要去lock金鎖。
- 此時此刻,死鎖就產生了。
- (3) 執行緒A因為拿不到銀鎖頭,流程走不下去(所有後邊程式碼有解鎖金鎖鎖頭的但是流程走不下去,所以金鎖頭解不開)。
- (4) 執行緒B因為拿不到金鎖頭,流程走不下去(所有後邊程式碼有解鎖銀鎖鎖頭的但是流程走不下去,所以銀鎖頭解不開)。
- 大家都晾在這裡,你等我,我等你。
死鎖演示
#include <iostream> #include <string> #include <thread> #include <vector> #include <list> #include <mutex> using namespace std; class A { public: //把收到的訊息(玩家命令)加入到一個佇列的執行緒 void inMsgRecvQueue() { for (int i = 1; i < 10000; ++i) { cout << "inMsgRecvQueue執行了,插入一個元素" << i << endl; my_mutex2.lock(); //其他程式碼 my_mutex1.lock(); msgRecvQueue.push_back(i); //假設這個數字就是玩家發來的命令,加入到訊息佇列中 my_mutex1.unlock(); //其他程式碼 my_mutex2.unlock(); } } //在這個函式中加鎖 bool outMsgMutPro(int& command ) { my_mutex1.lock(); //其他程式碼 my_mutex2.lock(); if (!msgRecvQueue.empty()) { //訊息佇列不為空 command = msgRecvQueue.front(); //返回第一個元素,但不檢查元素是否存在 msgRecvQueue.pop_front(); //移除第一個元素,但不返回 my_mutex2.unlock(); //其他程式碼 my_mutex1.unlock(); return true; } my_mutex2.unlock(); //其他程式碼 my_mutex1.unlock(); return false; } //把訊息從訊息佇列中取出的執行緒 void outMsgRecvQueue() { int command{}; for (int i = 1; i < 10000; ++i) { bool ret = outMsgMutPro(command); if (ret) { cout << "outMsgMutPro執行了,取出一個元素" << command << endl; //這裡就針對具體的命令具體處理 //... } else { //訊息佇列為空 cout << "outMsgRecvQueue執行了,但是當前訊息佇列為空" << i << endl; } } cout << "outMsgRecvQueue()執行完畢" << endl; } private: std::list<int> msgRecvQueue; //容器(訊息佇列),專門代表玩家給我們發來的命令 std::mutex my_mutex1; std::mutex my_mutex2; }; int main() { A obja; std::thread outMsgThread(&A::outMsgRecvQueue, &obja); //第二個引數是引用,保證執行緒裡操作同一個物件 std::thread inMsgThread(&A::inMsgRecvQueue, &obja); inMsgThread.join(); outMsgThread.join(); //主執行緒執行 std::cout << "主執行緒結束" << std::endl; return 0; }
死鎖的一般解決方案
- 只要保證這兩個互斥量上鎖的順序一致就不會死鎖。
std::lock()函式模板
- 用來處理多個互斥量的時候才出場。
- 能力:一次鎖住兩個或者兩個以上的互斥量(至少兩個,多了不限,1個不行)。
- 它不存在這種因為再多個執行緒中 因為鎖的順序問題導致死鎖的風險問題。
- std::lock():
- 如果互斥量中有一個沒鎖住,它就在那裡等著,等所有互斥量都鎖住,它才能往下走(返回)。
- 要麼兩個互斥量都鎖住,要麼兩個互斥量都沒鎖住。
- 如果只鎖了一個,另外一個沒鎖成功,則它立即把已經鎖住的解鎖。
class A { public: //把收到的訊息(玩家命令)加入到一個佇列的執行緒 void inMsgRecvQueue() { for (int i = 1; i < 10000; ++i) { cout << "inMsgRecvQueue執行了,插入一個元素" << i << endl; std::lock(my_mutex1, my_mutex2); msgRecvQueue.push_back(i); //假設這個數字就是玩家發來的命令,加入到訊息佇列中 my_mutex1.unlock(); //其他程式碼 my_mutex2.unlock(); } } //在這個函式中加鎖 bool outMsgMutPro(int& command ) { std::lock(my_mutex1, my_mutex2); if (!msgRecvQueue.empty()) { //訊息佇列不為空 command = msgRecvQueue.front(); //返回第一個元素,但不檢查元素是否存在 msgRecvQueue.pop_front(); //移除第一個元素,但不返回 my_mutex2.unlock(); //其他程式碼 my_mutex1.unlock(); return true; } my_mutex2.unlock(); //其他程式碼 my_mutex1.unlock(); return false; } //把訊息從訊息佇列中取出的執行緒 void outMsgRecvQueue() { int command{}; for (int i = 1; i < 10000; ++i) { bool ret = outMsgMutPro(command); if (ret) { cout << "outMsgMutPro執行了,取出一個元素" << command << endl; //這裡就針對具體的命令具體處理 //... } else { //訊息佇列為空 cout << "outMsgRecvQueue執行了,但是當前訊息佇列為空" << i << endl; } } cout << "outMsgRecvQueue()執行完畢" << endl; } private: std::list<int> msgRecvQueue; //容器(訊息佇列),專門代表玩家給我們發來的命令 std::mutex my_mutex1; std::mutex my_mutex2; };
std::lock_guard的std::adopt_lock引數
- std::adopt_lock是個結構體物件,起一個標記作用:作用就是表示這個互斥量已經lock(),不需要再std::lock_guard<std::mutext>建構函式裡 再面對物件進行再次lock()了。
class A { public: //把收到的訊息(玩家命令)加入到一個佇列的執行緒 void inMsgRecvQueue() { for (int i = 1; i < 10000; ++i) { cout << "inMsgRecvQueue執行了,插入一個元素" << i << endl; std::lock(my_mutex1, my_mutex2); std::lock_guard<mutex> in_mutex_guard1(my_mutex1, std::adopt_lock); std::lock_guard<mutex> in_mutex_guard2(my_mutex2, std::adopt_lock); msgRecvQueue.push_back(i); //假設這個數字就是玩家發來的命令,加入到訊息佇列中 //其他程式碼 } } //在這個函式中加鎖 bool outMsgMutPro(int& command ) { std::lock(my_mutex1, my_mutex2); std::lock_guard<mutex> out_mutex_guard1(my_mutex1, std::adopt_lock); std::lock_guard<mutex> out_mutex_guard2(my_mutex2, std::adopt_lock); if (!msgRecvQueue.empty()) { //訊息佇列不為空 command = msgRecvQueue.front(); //返回第一個元素,但不檢查元素是否存在 msgRecvQueue.pop_front(); //移除第一個元素,但不返回 return true; } return false; } //把訊息從訊息佇列中取出的執行緒 void outMsgRecvQueue() { int command{}; for (int i = 1; i < 10000; ++i) { bool ret = outMsgMutPro(command); if (ret) { cout << "outMsgMutPro執行了,取出一個元素" << command << endl; //這裡就針對具體的命令具體處理 //... } else { //訊息佇列為空 cout << "outMsgRecvQueue執行了,但是當前訊息佇列為空" << i << endl; } } cout << "outMsgRecvQueue()執行完畢" << endl; } private: std::list<int> msgRecvQueue; //容器(訊息佇列),專門代表玩家給我們發來的命令 std::mutex my_mutex1; std::mutex my_mutex2; };
- std::lock():一次鎖定多個互斥量,謹慎使用(建議一個一個鎖)。