【学习笔记】C++并发与多线程笔记四:互斥量(概念、用法、死锁)

一、前言

本文接上文 【学习笔记】C++并发与多线程笔记三:数据共享 的内容,主要包含互斥量的基本概念、用法、死锁演示以及解决方案。

二、互斥量的基本概念

互斥量就是个类对象,可以理解成一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。

备注:互斥量使用要小心,上锁的代码需要根据实际情况考虑(只保护需要保护的数据),少了达不到效果,多了影响效率。

三、互斥量的用法

首先,需要包含头文件#include <mutex>,然后使用 mutex 类即可创建锁对象:

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

class A
{
private:
    mutex my_mutex;         /* 创建一个互斥锁 */
};

3.1 lock()和unlock()

在代码中 lock() (上锁)和 unlock() (解锁)必须成对使用,步骤如下:

  • lock() 上锁;
  • 然后操作共享数据;
  • unlock() 解锁

备注:代码中使用互斥量的时绝不允许非对称调用,即 lock()unlock() 一定是成对出现的。

以上篇文章的示例代码为例,我们需要保护的共享数据为消息队列 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); /* 假设数字 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;
    }
    /* 把数据从消息队列中取出 */
    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; /* 容器(实际上是双向链表):存放玩家发生命令的队列 */
    mutex my_mutex;         /* 创建一个互斥锁 */
};

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类模板

我们在代码中上锁后,一定要记得解锁,如果忘记解锁会导致程序运行异常,而且通常很难排查。为了防止开发者忘记解锁,C++11引入了一个叫做 std::lock_guard 的类模板,它在开发者忘记解锁的时候,会替开发者自动解锁。

备注: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); /* 假设数字 i 就是收到的玩家命令 */
        }
    }
    /* 消息队列不为空时,返回并弹出第一个元素 */
    bool outMsgLULProc(int &command)
    {
        /**
         * m_guard 是一个 lock_guard 对象。
         * lock_guard 构造函数里执行了 lock()。
         * lock_guard 析构函数里执行了 unlock()。
         */
        lock_guard<mutex> m_guard(my_mutex);
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front(); /* 返回第一个元素 */
            msgRecvQueue.pop_front();           /* 移除第一个元素 */
            return true;
        }
        return false;
    }
    /* 把数据从消息队列中取出 */
    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; /* 容器(实际上是双向链表):存放玩家发生命令的队列 */
    mutex my_mutex;         /* 创建一个互斥锁 */
};

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 虽然用起来方便,但是不够灵活,它只能在析构函数中 unlock(),也就是对象被释放的时候,这通常是在函数返回的时候,或者通过添加代码块 { /* 代码块 */ } 限定作用域来指定释放时机。

四、死锁

一个简单的例子:

  • 张三在北京说:等李四来了之后,我就去广东。
  • 李四在广东说:等张三来了之后,我就去北京。

这两个人一直等待对方就形成了死锁。

同理,假如在代码中有两把锁(至少有两个互斥量存在才会产生死锁)分别称为锁1、锁2,并且有两个线程分别称为线程A和线程B。只有在某个线程同时获得锁1和锁2时,才能完成某项工作:

  • 线程A执行时,先上锁1,再上锁2。
  • 线程B执行时,先上锁2,再上锁1。

若在程序执行线程A的过程中,上好了锁1后,出现了上下文切换,系统调度转去执行线程B,把锁2给上了,那么后续线程A拿不到锁2,线程B拿不到锁1,两条线程都没法往下执行,即出现了死锁。

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(); /* 实际代码中,两把锁不一定同时上,它们可能保护不同的数据 */
            m_mutex2.lock();
            msgRecvQueue.push_back(i); /* 假设数字 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;
    }
    /* 把数据从消息队列中取出 */
    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; /* 容器(实际上是双向链表):存放玩家发生命令的队列 */
    mutex m_mutex1;         /* 创建互斥量1 */
    mutex m_mutex2;         /* 创建互斥量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 死锁的一般解决方案

通常来讲,只要保证多个互斥量上锁的顺序一致,就不会出现死锁,比如把上面示例代码的两个线程回调函数中的上锁顺序改一下,保持一致就好了(都改为先上锁1,再上锁2)。

4.3 std::lock()函数模板

std::lock() 函数模板是C++11引入的,它能一次锁住两个或两个以上的互斥量,并且它不存在上述的在多线程中由于上锁顺序问题造成的死锁现象,原因如下:

std::lock() 函数模板在锁定两个互斥量时,只有两种情况:

  1. 两个互斥量都没有锁住;
  2. 两个互斥量都被锁住。

如果只锁了一个,另一个没锁成功,则它会立即把已经锁住的互斥量解锁。

    /* 把收到的消息(玩家命令)存到队列中 */
    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); /* 假设数字 i 就是收到的玩家命令 */
            m_mutex2.unlock();         /* 这里别忘记解锁 */
            m_mutex1.unlock();         /* 这里别忘记解锁 */
        }
    }

4.4 std::lock_guard的std::adopt_lock参数

在使用 std::lock() 函数模板锁上多个互斥量时,也必须得记得把每个互斥量解锁,此时借助 std::lock_guardstd::adopt_lock 参数可以省略解锁的代码。

    /* 把收到的消息(玩家命令)存到队列中 */
    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); /* 构造时不上锁,但析构时解锁 */
            std::lock_guard<std::mutex> m_guard2(m_mutex2, std::adopt_lock); /* 构造时不上锁,但析构时解锁 */
            msgRecvQueue.push_back(i);                                       /* 假设数字 i 就是收到的玩家命令 */
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容