【學習筆記】C++并發(fā)與多線程筆記四:互斥量(概念肢础、用法、死鎖)

一碌廓、前言

本文接上文 【學習筆記】C++并發(fā)與多線程筆記三:數(shù)據(jù)共享 的內(nèi)容传轰,主要包含互斥量的基本概念、用法谷婆、死鎖演示以及解決方案慨蛙。

二、互斥量的基本概念

互斥量就是個類對象波材,可以理解成一把鎖股淡,多個線程嘗試用lock()成員函數(shù)來加鎖,只有一個線程能鎖定成功廷区,如果沒有鎖成功唯灵,那么流程將卡在lock()這里不斷嘗試去鎖定。

備注:互斥量使用要小心隙轻,上鎖的代碼需要根據(jù)實際情況考慮(只保護需要保護的數(shù)據(jù))埠帕,少了達不到效果,多了影響效率玖绿。

三敛瓷、互斥量的用法

首先,需要包含頭文件#include <mutex>斑匪,然后使用 mutex 類即可創(chuàng)建鎖對象:

#include <iostream>
#include <mutex>
using namespace std;

class A
{
private:
    mutex my_mutex;         /* 創(chuàng)建一個互斥鎖 */
};

3.1 lock()和unlock()

在代碼中 lock() (上鎖)和 unlock() (解鎖)必須成對使用呐籽,步驟如下:

  • lock() 上鎖;
  • 然后操作共享數(shù)據(jù)蚀瘸;
  • unlock() 解鎖

備注:代碼中使用互斥量的時絕不允許非對稱調(diào)用狡蝶,即 lock()unlock() 一定是成對出現(xiàn)的。

以上篇文章的示例代碼為例贮勃,我們需要保護的共享數(shù)據(jù)為消息隊列 msgRecvQueue 贪惹,在讀寫這個隊列前就需要上鎖,讀寫完畢后需要解鎖寂嘉,代碼如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;

class A
{
public:
    /* 把收到的消息(玩家命令)存到隊列中 */
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            cout << "inMsgRecvQueue exec, push an elem " << i << endl;
            my_mutex.lock();           /* 上鎖 */
            msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
            my_mutex.unlock();         /* 解鎖 */
        }
    }
    /* 消息隊列不為空時奏瞬,返回并彈出第一個元素 */
    bool outMsgLULProc(int &command)
    {
        my_mutex.lock(); /* 上鎖 */
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front(); /* 返回第一個元素 */
            msgRecvQueue.pop_front();           /* 移除第一個元素 */
            my_mutex.unlock();                  /* 解鎖(每個分支都需要解鎖,別漏了) */
            return true;
        }
        my_mutex.unlock(); /* 解鎖(每個分支都需要解鎖泉孩,別漏了) */
        return false;
    }
    /* 把數(shù)據(jù)從消息隊列中取出 */
    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; ++i)
        {
            bool result = outMsgLULProc(command);
            if (result)
                cout << "outMsgLULProc exec, and pop_front: " << command << endl;
            else
                cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
            cout << "outMsgRecvQueue exec end!" << i << endl;
        }
    }

private:
    list<int> msgRecvQueue; /* 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 */
    mutex my_mutex;         /* 創(chuàng)建一個互斥鎖 */
};

int main()
{
    A obj;
    thread myInMsgObj(&A::inMsgRecvQueue, &obj);
    thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
    myInMsgObj.join();
    myOutMsgObj.join();

    cout << "Hello World!" << endl;
    return 0;
}

3.2 std::lock_guard類模板

我們在代碼中上鎖后硼端,一定要記得解鎖,如果忘記解鎖會導致程序運行異常寓搬,而且通常很難排查显蝌。為了防止開發(fā)者忘記解鎖,C++11引入了一個叫做 std::lock_guard 的類模板,它在開發(fā)者忘記解鎖的時候曼尊,會替開發(fā)者自動解鎖酬诀。

備注:std::lock_guard 可以直接取代 lock()unlock(),也就說使用 std::lock_guard 后骆撇,就不能再使用 lock()unlock() 了瞒御。

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;

class A
{
public:
    /* 把收到的消息(玩家命令)存到隊列中 */
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            cout << "inMsgRecvQueue exec, push an elem " << i << endl;
            lock_guard<mutex> m_guard(my_mutex);
            msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
        }
    }
    /* 消息隊列不為空時,返回并彈出第一個元素 */
    bool outMsgLULProc(int &command)
    {
        /**
         * m_guard 是一個 lock_guard 對象神郊。
         * lock_guard 構(gòu)造函數(shù)里執(zhí)行了 lock()肴裙。
         * lock_guard 析構(gòu)函數(shù)里執(zhí)行了 unlock()。
         */
        lock_guard<mutex> m_guard(my_mutex);
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front(); /* 返回第一個元素 */
            msgRecvQueue.pop_front();           /* 移除第一個元素 */
            return true;
        }
        return false;
    }
    /* 把數(shù)據(jù)從消息隊列中取出 */
    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; ++i)
        {
            bool result = outMsgLULProc(command);
            if (result)
                cout << "outMsgLULProc exec, and pop_front: " << command << endl;
            else
                cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
            cout << "outMsgRecvQueue exec end!" << i << endl;
        }
    }

private:
    list<int> msgRecvQueue; /* 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 */
    mutex my_mutex;         /* 創(chuàng)建一個互斥鎖 */
};

int main()
{
    A obj;
    thread myInMsgObj(&A::inMsgRecvQueue, &obj);
    thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
    myInMsgObj.join();
    myOutMsgObj.join();

    cout << "Hello World!" << endl;
    return 0;
}

std::lock_guard 雖然用起來方便涌乳,但是不夠靈活蜻懦,它只能在析構(gòu)函數(shù)中 unlock(),也就是對象被釋放的時候夕晓,這通常是在函數(shù)返回的時候宛乃,或者通過添加代碼塊 { /* 代碼塊 */ } 限定作用域來指定釋放時機。

四蒸辆、死鎖

一個簡單的例子:

  • 張三在北京說:等李四來了之后征炼,我就去廣東。
  • 李四在廣東說:等張三來了之后躬贡,我就去北京谆奥。

這兩個人一直等待對方就形成了死鎖。

同理拂玻,假如在代碼中有兩把鎖(至少有兩個互斥量存在才會產(chǎn)生死鎖)分別稱為鎖1酸些、鎖2,并且有兩個線程分別稱為線程A和線程B檐蚜。只有在某個線程同時獲得鎖1和鎖2時魄懂,才能完成某項工作:

  • 線程A執(zhí)行時,先上鎖1熬甚,再上鎖2逢渔。
  • 線程B執(zhí)行時肋坚,先上鎖2乡括,再上鎖1。

若在程序執(zhí)行線程A的過程中智厌,上好了鎖1后诲泌,出現(xiàn)了上下文切換,系統(tǒng)調(diào)度轉(zhuǎn)去執(zhí)行線程B铣鹏,把鎖2給上了敷扫,那么后續(xù)線程A拿不到鎖2,線程B拿不到鎖1,兩條線程都沒法往下執(zhí)行葵第,即出現(xiàn)了死鎖绘迁。

4.1 死鎖演示

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;

class A
{
public:
    /* 把收到的消息(玩家命令)存到隊列中 */
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            cout << "inMsgRecvQueue exec, push an elem " << i << endl;
            m_mutex1.lock(); /* 實際代碼中,兩把鎖不一定同時上卒密,它們可能保護不同的數(shù)據(jù) */
            m_mutex2.lock();
            msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
            m_mutex2.unlock();
            m_mutex1.unlock();
        }
    }
    /* 消息隊列不為空時缀台,返回并彈出第一個元素 */
    bool outMsgLULProc(int &command)
    {
        m_mutex2.lock();
        m_mutex1.lock();
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front(); /* 返回第一個元素 */
            msgRecvQueue.pop_front();           /* 移除第一個元素 */
            m_mutex1.unlock();
            m_mutex2.unlock();
            return true;
        }
        m_mutex1.unlock();
        m_mutex2.unlock();
        return false;
    }
    /* 把數(shù)據(jù)從消息隊列中取出 */
    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; ++i)
        {
            bool result = outMsgLULProc(command);
            if (result)
                cout << "outMsgLULProc exec, and pop_front: " << command << endl;
            else
                cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
            cout << "outMsgRecvQueue exec end!" << i << endl;
        }
    }

private:
    list<int> msgRecvQueue; /* 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 */
    mutex m_mutex1;         /* 創(chuàng)建互斥量1 */
    mutex m_mutex2;         /* 創(chuàng)建互斥量2 */
};

int main()
{
    A obj;
    thread myInMsgObj(&A::inMsgRecvQueue, &obj);
    thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
    myInMsgObj.join();
    myOutMsgObj.join();

    cout << "Hello World!" << endl;
    return 0;
}

4.2 死鎖的一般解決方案

通常來講,只要保證多個互斥量上鎖的順序一致哮奇,就不會出現(xiàn)死鎖膛腐,比如把上面示例代碼的兩個線程回調(diào)函數(shù)中的上鎖順序改一下,保持一致就好了(都改為先上鎖1鼎俘,再上鎖2)哲身。

4.3 std::lock()函數(shù)模板

std::lock() 函數(shù)模板是C++11引入的,它能一次鎖住兩個或兩個以上的互斥量贸伐,并且它不存在上述的在多線程中由于上鎖順序問題造成的死鎖現(xiàn)象勘天,原因如下:

std::lock() 函數(shù)模板在鎖定兩個互斥量時,只有兩種情況:

  1. 兩個互斥量都沒有鎖坠髫ぁ误辑;
  2. 兩個互斥量都被鎖住。

如果只鎖了一個歌逢,另一個沒鎖成功巾钉,則它會立即把已經(jīng)鎖住的互斥量解鎖。

    /* 把收到的消息(玩家命令)存到隊列中 */
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            cout << "inMsgRecvQueue exec, push an elem " << i << endl;
            std::lock(m_mutex1, m_mutex2);
            msgRecvQueue.push_back(i); /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
            m_mutex2.unlock();         /* 這里別忘記解鎖 */
            m_mutex1.unlock();         /* 這里別忘記解鎖 */
        }
    }

4.4 std::lock_guard的std::adopt_lock參數(shù)

在使用 std::lock() 函數(shù)模板鎖上多個互斥量時秘案,也必須得記得把每個互斥量解鎖砰苍,此時借助 std::lock_guardstd::adopt_lock 參數(shù)可以省略解鎖的代碼。

    /* 把收到的消息(玩家命令)存到隊列中 */
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            cout << "inMsgRecvQueue exec, push an elem " << i << endl;
            std::lock(m_mutex1, m_mutex2);                                   /* 鎖上兩個互斥量 */
            std::lock_guard<std::mutex> m_guard1(m_mutex1, std::adopt_lock); /* 構(gòu)造時不上鎖阱高,但析構(gòu)時解鎖 */
            std::lock_guard<std::mutex> m_guard2(m_mutex2, std::adopt_lock); /* 構(gòu)造時不上鎖赚导,但析構(gòu)時解鎖 */
            msgRecvQueue.push_back(i);                                       /* 假設(shè)數(shù)字 i 就是收到的玩家命令 */
        }
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赤惊,隨后出現(xiàn)的幾起案子吼旧,更是在濱河造成了極大的恐慌,老刑警劉巖未舟,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圈暗,死亡現(xiàn)場離奇詭異,居然都是意外死亡裕膀,警方通過查閱死者的電腦和手機员串,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昼扛,“玉大人寸齐,你說我怎么就攤上這事。” “怎么了渺鹦?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵扰法,是天一觀的道長。 經(jīng)常有香客問我毅厚,道長迹恐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任卧斟,我火速辦了婚禮殴边,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘珍语。我一直安慰自己锤岸,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布板乙。 她就那樣靜靜地躺著是偷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪募逞。 梳的紋絲不亂的頭發(fā)上蛋铆,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音放接,去河邊找鬼刺啦。 笑死,一個胖子當著我的面吹牛纠脾,可吹牛的內(nèi)容都是我干的玛瘸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼苟蹈,長吁一口氣:“原來是場噩夢啊……” “哼糊渊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起慧脱,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤渺绒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后菱鸥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宗兼,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年采缚,在試婚紗的時候發(fā)現(xiàn)自己被綠了针炉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挠他。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡扳抽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贸呢,我是刑警寧澤镰烧,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站楞陷,受9級特大地震影響怔鳖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜固蛾,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一结执、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧艾凯,春花似錦献幔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恃泪,卻和暖如春郑兴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贝乎。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工情连, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人览效。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓蒙具,卻偏偏與公主長得像,于是被迫代替她去往敵國和親朽肥。 傳聞我的和親對象是個殘疾皇子禁筏,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容