互斥量概念、用法、死鎖演示及解決詳解

一.互斥量的概念

保護共享數據,操作時,某個線程用代碼把共享數據鎖住、操作數據、解鎖
其他線程想操作共享數據必須等待解鎖。
互斥量
是個類對象,理解成一把鎖,多個線程嘗試用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()一次鎖定多個互斥量;謹慎使用(建議一個一個鎖),因爲不同的互斥量控制不同的共享數據,兩個互斥量在一起的情況不多見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章