1. 程式人生 > 其它 >(C++11/14/17學習筆記):互斥量概念、用法、死鎖演示及解決詳解

(C++11/14/17學習筆記):互斥量概念、用法、死鎖演示及解決詳解

技術標籤:C++11/14/17

目錄

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

互斥量(mutex)的基本概念

互斥量的用法

lock(), unlock()

std:lock_guard類模板

死鎖

死鎖演示

死鎖的一般解決方案

std::lock()函式模板

std::lock_guard的std::adopt_lock引數

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

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