多线程共享数据的保护+互斥量概念、用法、死锁演示及解决详解(std::lock()、std::adopt_lock)

#include <list>
#include <thread>

class A
{
public:
    //把收到的消息(玩家命令)入到一个队列的线程
    void inMsgRecvQueue()
    {
        for (int i = 0;i < 100000;++i)
        {
            cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);
        }
    }

    //把数据从消息队列中取出的线程
    void outMsgRecvQueue()
    {
        for (int i = 0;i < 100;i++)
        {
            if (!msgRecvQueue.empty())
            {
                int command = msgRecvQueue.front();
                msgRecvQueue.pop_front();
                //这里考虑处理数据
            }
            else
            {
                cout << "outMsgRecvQueue()执行,但是目前消息队列中为空" << endl;
            }
        }
        
    }

private:
    std::list<int> msgRecvQueue;    //容器,专门用于代表玩家给咱们发送过来的命令
};

int main()
{
    A myobja;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobja);
    std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
    myInMsgObj.join();
    myOutnMsgObj.join();

    return 0;
}

保护共享数据,操作时某个线程用代码把共享数据锁住,操作数据、解锁;
其他想操作共享数据的线程必须等待解锁,锁定住,操作,解锁。

一、互斥量的概念

互斥量(mutex)的基本概念,多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标志是lock()函数返回)
如果没锁成功,那么流程卡在lock()这里不断尝试去锁这把锁头

二、互斥量的用法

#include <mutex>

2.1 lock() unlock()

步骤:先lock(),操作共享数据,再unlock(),每调用一次lock(),必然应该调用一个unlock()

#include <list>
#include <thread>
#include <mutex>

class A
{
public:
    //把收到的消息(玩家命令)入到一个队列的线程
    void inMsgRecvQueue()
    {
        for (int i = 0;i < 100;++i)
        {
            cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
            my_mutex.lock();
            msgRecvQueue.push_back(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 < 100;i++)
        {
            bool result = outMsgLULProc(command);
            if (result == true)
            {
                cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
                //可以考虑对命令(数据)进行处理
            }
            else
            {
                cout << "outMsgRecvQueue()执行,但是目前消息队列中为空" << i << endl;
            }
        }
        cout << "end" << endl;
    }

private:
    std::list<int> msgRecvQueue;    //容器,专门用于代表玩家给咱们发送过来的命令
    std::mutex my_mutex; //创建一个互斥量 
};

int main()
{
    A myobja;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobja);
    std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
    myInMsgObj.join();
    myOutnMsgObj.join();

    return 0;
}

为了防止大家忘记unlock(),引入了一个叫std::lock_guard()的类模板:你忘记unlock()不要紧,我替你unlock()。
学习过智能指针(unique_ptr):你忘记释放内存不要紧,我替你释放。

2.1 std::lock_guard类模板,直接取代lock()和unlock(),也就是说,用了lock_guard以后,再不能使用lock()和unlock()

记住这种写法:

std::lock_guard<std::mutex> sguard(my_mutex);

其中,my_mutex的定义在:

std::mutex my_mutex1; //创建一个互斥量 

生成std::lock_guard<std::mutex>类模板的对象肯定要调用类的构造函数,构造函数里执行了mutex::lock()
sguard是局部对象,局部对象退出函数(return的时候)需要析构,析构函数调用了mutex::unlock()

    bool outMsgLULProc(int& command)
    {
        // my_mutex.lock();
        std::lock_guard<std::mutex> sguard(my_mutex);
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front();
            msgRecvQueue.pop_front();
            // my_mutex.unlock();
            return true;
        }
        // my_mutex.unlock();
        return false; 
    }

lock_guard不够灵活:
lock()、unlock()随时能调用,随时能解锁
lock_guard只能在析构的时候调用解锁

通过大括号可以使得只要到大括号末尾lock_guard就会析构掉
原则:在互斥量包裹的位置之外,不要动共享数据

三、死锁

张三:站在北京等李四,不挪窝;
李四:站在北京等张三,不挪窝;
C++中:
比如有两把锁(死锁这个问题,至少有两个锁头 两个互斥量才能产生)
两个线程A和B

(1)线程A执行的时候,这个线程先锁金锁,把金锁lock()成功了,然后去lock银锁;
出现了上下文切换(此时银锁还没有锁就出现了上下文切换)
个人理解:关键在于两个lock之间发生了上下文切换
(2)线程B执行了,这个线程先锁银锁,因为银锁还没有被锁,所以银锁会lock成功,线程B要去lock金锁
此时此刻,死锁就产生了;
(3)线程A因为拿不到银锁头,流程走不下去(后边代码有解锁金锁头但是流程走不下去,所以金锁头解不开)
(4)线程B因为拿不到金锁头,流程走不下去(后边代码有解锁银锁头但是流程走不下去,所以银锁头解不开)

3.1 死锁演示

#include <list>
#include <thread>
#include <mutex>

class A
{
public:
    //把收到的消息(玩家命令)入到一个队列的线程
    void inMsgRecvQueue()
    {
        for (int i = 0;i < 100000;++i)
        {
            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())
        {
            command = msgRecvQueue.front();
            msgRecvQueue.pop_front();
            my_mutex1.unlock();
            my_mutex2.unlock();
            return true;
        }
        my_mutex1.unlock();
        my_mutex2.unlock(); 
        return false; 
    }

    //把数据从消息队列中取出的线程
    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0;i < 100000;i++)
        {
            bool result = outMsgLULProc(command);
            if (result == true)
            {
                cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
                //可以考虑对命令(数据)进行处理
            }
            else
            {
                cout << "outMsgRecvQueue()执行,但是目前消息队列中为空" << i << endl;
            }
        }
        cout << "end" << endl;
    }

private:
    std::list<int> msgRecvQueue;    //容器,专门用于代表玩家给咱们发送过来的命令
    std::mutex my_mutex1; //创建一个互斥量 
    std::mutex my_mutex2;
};

int main()
{
    A myobja;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobja);
    std::thread myInMsgObj(&A::inMsgRecvQueue,&myobja);
    myInMsgObj.join();
    myOutnMsgObj.join();

    return 0;
}

3.2 死锁的一般解决方案

只要保证两个互斥量上锁的顺序保持一致就不会死锁。

3.3 std::lock()函数模板

用来处理多个互斥量的时候才出场

能力:一次锁住两个或者两个以上互斥量(至少两个,多了不限,1个不行)
它不存在这种因为在多个线程中,因为锁的问题导致死锁的风险问题;
std::lock():如果互斥量中有一个没锁柱,他就在那里等着,等着所有互斥量都锁住,他才能往下走(返回)
特点:要么两个互斥量都锁住,要么两个互斥量都没锁住 。
要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。

std::lock(my_mutex1,my_mutex2);
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();

std::lock 如果遇到一个锁住,一个没锁住,就会把没锁住的那个撒开

3.4 std::lock_guard的std::adopt_lock参数

 std::lock(my_mutex1,my_mutex2);
std::lock_guard<std::mutex> sguard1(my_mutex1,std::adopt_lock);
std::lock_guard<std::mutex> sguard2(my_mutex2,std::adopt_lock);
msgRecvQueue.push_back(i);
// my_mutex2.unlock();
 // my_mutex1.unlock();

曾经讲过

std::lock_guard<std::mutex> sguard1(my_mutex1);

该行代码的含义是:
在该对象的构造函数中会调用互斥量my_mutex1的std::mutex::lock()函数,在析构函数中会调用互斥量my_mutex1的std::mutex::unlock()函数。

而现在的情况是:
std::lock(my_mutex1,my_mutex2);相当于把两个互斥量的std::mutex::lock()函数调用完了,就不希望std::lock_guard<std::mutex> sguard1(my_mutex1);对象的构造函数中再调用std::mutex::lock()函数
而该对象析构时照常,调用my_mutex1和my_mutex2的unlock()函数

std::adopt_lock()是个结构体对象,起一个标记作用。
作用就表示这个互斥量已经lock(),不需要在
std::lock_guard<std::mutex>的对象的构造函数里对mutex类对象再lock()了。

总结:
std::lock()一次锁定多个互斥量,谨慎使用(建议一个一个锁)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值