一.互斥量的概念
保護共享數據,操作時,某個線程用代碼把共享數據鎖住、操作數據、解鎖
其他線程想操作共享數據必須等待解鎖。
互斥量:
是個類對象,理解成一把鎖,多個線程嘗試用lock()來加鎖,只有一個線程會加鎖成功,成功的標誌就是lock()返回,如果不成功,會一直阻塞在lock()處。
互斥量使用要小心,保護數據不多也不能少,少了,沒有達到保護的效果,多了,影響效率。
二.互斥量的用法
1.lock()、unlock()
步驟:先lock(),操作共享數據,unlock()
lock()與unlock()要成對使用,有lock()必然要有unlock(),每調用一次lock(),必然應該調用一次unlock();不應該也不允許調用一次lock(),卻調用了2次unlock(),這些非對稱數量的調用都會導致代碼不穩定甚至崩潰。實例代碼如下:
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成員函數作爲線程的入口函數
class A{
public:
//把收到的玩家命令放到隊列的一個線程 寫
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模擬了100000次
cout<<"inMsgRecvQueue執行,插入一個元素:"<<i<<endl;
my_mutex.lock(); //保護共享數據
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
}
bool outMsgLULProc(int &command)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不爲空
int command = msgRecvQueue.front();//返回第一個元素,但不檢查元素是否存在
msgRecvQueue.pop_front();//移除第一個元素。但不返回;
my_mutex.unlock(); //所有分支都必須有unlock()
return true;
}
my_mutex.unlock();
return false;
}
//處理玩家命令的線程 讀
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模擬了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()執行,取出一個元素"<< endl;
//處理數據
}else{
cout<<"outMsgRecvQueue執行,消息隊列爲空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家發送的命令。需要保護的共享數據
mutex my_mutex; //創建一個互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
myIn.join();
myOut.join();
return 0;
}
lock()之後,必須進行unlock()。
爲了防止大家忘記unlock(),引入了一個叫std::lock_guard的類模板:忘記unlock(),替你unlock()。如同智能指針(unique_ptr<>),你忘記釋放內存不要緊,我替你釋放。
2.lock_guard類模板
直接取代lock()和unlock(),用了lock_guard之後,不能再使用lock()和unlock()。
生成lock_guard對象的時候,調用構造函數,構造函數中會執行lock(),當析構的時候,會執行unlock()。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成員函數作爲線程的入口函數
class A{
public:
//把收到的玩家命令放到隊列的一個線程 寫
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模擬了100000次
cout<<"inMsgRecvQueue執行,插入一個元素:"<<i<<endl;
//my_mutex.lock(); //保護共享數據
{
lock_guard<mutex> m(my_mutex);
msgRecvQueue.push_back(i);
} //提前結束lock_guard對象,析構調用unlock()
//my_mutex.unlock();
//其他代碼
}
}
bool outMsgLULProc(int &command)
{
lock_guard<mutex> m(my_mutex);
//my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不爲空
int command = msgRecvQueue.front();//返回第一個元素,但不檢查元素是否存在
msgRecvQueue.pop_front();//移除第一個元素。但不返回;
//my_mutex.unlock(); //所有分支都必須有unlock()
return true;
}
//my_mutex.unlock();
return false;
}
//處理玩家命令的線程 讀
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模擬了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()執行,取出一個元素"<< endl;
//處理數據
}else{
cout<<"outMsgRecvQueue執行,消息隊列爲空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家發送的命令。需要保護的共享數據
mutex my_mutex; //創建一個互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
myIn.join();
myOut.join();
return 0;
}
三.死鎖
產生死鎖,至少有兩個互斥量。
兩個線程A、B;兩把鎖:金鎖 ,銀鎖
(1)線程A執行的時候,這個線程先鎖金鎖,把金鎖lock()成功了,然後它去lock銀鎖。
(2)此時出現了上下文切換
(3)線程B執行了,這個線程先鎖銀鎖,因爲銀鎖還沒被鎖,所以銀鎖會lock()成功,線程B要去lock()金鎖。
(4)此時此刻,死鎖就產生了;
(5)線程A因爲拿不到銀鎖頭,流程走不下去(所有後邊代碼有解金鎖的但是流程走不下去,所以金鎖解不開)
(6)線程B因爲拿不到金鎖頭,流程走不下去(所有後邊代碼有解銀鎖的但是流程走不下去,所以銀鎖解不開)
大家都晾在那裏,你等我,我等你
1.死鎖的演示
以下代碼會產生死鎖,兩個線程上鎖的順序正好是反着的,解鎖順序無所謂。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成員函數作爲線程的入口函數
class A{
public:
//把收到的玩家命令放到隊列的一個線程 寫
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模擬了100000次
cout<<"inMsgRecvQueue執行,插入一個元素:"<<i<<endl;
my_mutex1.lock(); //保護共享數據
my_mutex2.lock(); //保護共享數據
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock(); //保護共享數據
}
}
bool outMsgLULProc(int &command)
{
my_mutex2.lock();
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
//消息不爲空
int command = msgRecvQueue.front();//返回第一個元素,但不檢查元素是否存在
msgRecvQueue.pop_front();//移除第一個元素。但不返回;
my_mutex1.unlock();
my_mutex2.unlock(); //所有分支都必須有unlock()
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//處理玩家命令的線程 讀
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模擬了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()執行,取出一個元素"<< endl;
//處理數據
}else{
cout<<"outMsgRecvQueue執行,消息隊列爲空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家發送的命令。需要保護的共享數據
mutex my_mutex1; //創建一個互斥量
mutex my_mutex2; //創建一個互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
myIn.join();
myOut.join();
return 0;
}
2.死鎖的一般解決方案
- 保證兩個互斥量的加鎖順序是一致的
3.lock()函數模板
用來處理多個互斥量的時候纔出場。
一次鎖住兩個或者兩個以上的互斥量(至少兩個),不存在因爲在多個線程中,因爲加鎖的順序的問題而導致的死鎖問題。---->一次性申請到所需要的全部資源,破壞死鎖的"保持和請求"條件。
std::lock():如果互斥量中有一個沒有鎖住,它就在那等着,等所有互斥量都鎖住,它才能往下走(返回);要麼兩個互斥量都鎖柱,要麼兩個互斥量都沒鎖住,如果只鎖了一個,另外一個沒有鎖成功,則它立即把已經鎖住的解鎖。
還是需要手動進行unlock()。
實例代碼如下:
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成員函數作爲線程的入口函數
class A{
public:
//把收到的玩家命令放到隊列的一個線程 寫
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模擬了100000次
cout<<"inMsgRecvQueue執行,插入一個元素:"<<i<<endl;
lock(my_mutex1,my_mutex2);
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock(); //保護共享數據
}
}
bool outMsgLULProc(int &command)
{
lock(my_mutex1,my_mutex2);
if (!msgRecvQueue.empty())
{
//消息不爲空
int command = msgRecvQueue.front();//返回第一個元素,但不檢查元素是否存在
msgRecvQueue.pop_front();//移除第一個元素。但不返回;
my_mutex1.unlock();
my_mutex2.unlock(); //所有分支都必須有unlock()
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//處理玩家命令的線程 讀
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模擬了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()執行,取出一個元素"<< endl;
//處理數據
}else{
cout<<"outMsgRecvQueue執行,消息隊列爲空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家發送的命令。需要保護的共享數據
mutex my_mutex1; //創建一個互斥量
mutex my_mutex2; //創建一個互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
myIn.join();
myOut.join();
return 0;
}
4.lock_guard的adopt_lock參數
adopt_lock是個結構體對象,起一個標誌作用:就是表示這個互斥量已經lock(),不需要在lock_guardstd::mutex構造函數裏再對mutex對象進行再次lock()了。實例代碼如下:
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成員函數作爲線程的入口函數
class A{
public:
//把收到的玩家命令放到隊列的一個線程 寫
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模擬了100000次
cout<<"inMsgRecvQueue執行,插入一個元素:"<<i<<endl;
lock(my_mutex1,my_mutex2);
lock_guard<mutex> m1(my_mutex1,adopt_lock);
lock_guard<mutex> m2(my_mutex2,adopt_lock);
msgRecvQueue.push_back(i);
}
}
bool outMsgLULProc(int &command)
{
lock(my_mutex1,my_mutex2);
lock_guard<mutex> m1(my_mutex1,adopt_lock);
lock_guard<mutex> m2(my_mutex2,adopt_lock);
if (!msgRecvQueue.empty())
{
//消息不爲空
int command = msgRecvQueue.front();//返回第一個元素,但不檢查元素是否存在
msgRecvQueue.pop_front();//移除第一個元素。但不返回;
//my_mutex1.unlock();
//my_mutex2.unlock(); //所有分支都必須有unlock()
return true;
}
// my_mutex1.unlock();
//my_mutex2.unlock();
return false;
}
//處理玩家命令的線程 讀
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模擬了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()執行,取出一個元素"<< endl;
//處理數據
}else{
cout<<"outMsgRecvQueue執行,消息隊列爲空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家發送的命令。需要保護的共享數據
mutex my_mutex1; //創建一個互斥量
mutex my_mutex2; //創建一個互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二個參數是引用,才能保證線程裏用的是同一個對象
myIn.join();
myOut.join();
return 0;
}
總結:lock()一次鎖定多個互斥量;謹慎使用(建議一個一個鎖),因爲不同的互斥量控制不同的共享數據,兩個互斥量在一起的情況不多見。