互斥量(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():一次鎖定多個互斥量,謹慎使用(建議一個一個鎖)。