天天看點

UNIX(多線程):06---互斥量概念、用法、死鎖示範及解決詳解

互斥量(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。
  1. #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)。
  1. #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因為拿不到金鎖頭,流程走不下去(所有後邊代碼有解鎖銀鎖鎖頭的但是流程走不下去,是以銀鎖頭解不開)。
  • 大家都晾在這裡,你等我,我等你。

死鎖示範

  1. #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():
  • 如果互斥量中有一個沒鎖住,它就在那裡等着,等所有互斥量都鎖住,它才能往下走(傳回)。
  • 要麼兩個互斥量都鎖住,要麼兩個互斥量都沒鎖住。
  • 如果隻鎖了一個,另外一個沒鎖成功,則它立即把已經鎖住的解鎖。
  1. 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()了。
  1. 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():一次鎖定多個互斥量,謹慎使用(建議一個一個鎖)。