美文网首页
多线程学习(四)

多线程学习(四)

作者: lxr_ | 来源:发表于2021-11-12 22:11 被阅读0次

互斥量

上一篇文章的最后的示例
假设做一个简易的网络游戏服务器:
两个自己创建的线程,一个线程收集玩家命令(用数字代表),并将命令写到一个队列中
另一个线程从队列中取出玩家发送来的命令,解析,然后执行玩家需要的动作

上一篇文章最后的示例演示发现如下问题
程序运行途中会出现崩溃,原因就是不断地往容器中写数据,同时不停地读数据,两个线程随意运行,一定会出问题。
假设写线程正在执行,而读线程又去读数据并删除数据,程序乱套,报出异常。
解决思路:在执行写数据线程的时候,让读数据线程等着。相反的情况就是,在执行读线程的时候,写数据线程等待。读完后再往这块共享内存中写数据。

解决办法
保护共享数据,操作时,用代码把共享数据锁住,再操作数据、最后解锁
其他想操作共享数据的线程必须等待解锁,然后再锁住操作数据,再解锁

一、互斥量(mutex)的基本概念

互斥量是个类对象,通俗理解为一把,多个线程尝试用lock()加锁只有一个线程能够锁定成功(lock()返回)
如果没锁成功,那么流程就卡在lock()这里不断尝试去给这把锁上锁
注意:互斥量(锁)保护数据不能多也不能少,少了,没达到保护效果,多了,影响效率

二、互斥量用法

2.1)lock(),unlock()
步骤:先lock(),操作共享数据,再unlock()
lock()和unlock()要成对使用,有lock()必然要有unlock(),没调用一次lock(),必然应该调用一次unlock();
为了防止忘记unlock(),引入了一个叫std::lock_guard()的类模板:可以实现智能unlock(),类似于智能指针unique_ptr<>
2.2)std::lock_guard类模板可以取代lock()和unlock(),用了lock_guard后,不能再使用lock()和unlock()
lock_guard构造函数里执行了mutex::lock(),析构函数里执行了mutex::unlock()
小技巧:可以加个{}提前析构lock_guard,实现提前unlock()

#include "stdafx.h"

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>

using namespace std;
#include <list>

//用成员函数作为线程函数的方法来写线程
class A
{
public:
    //线程:把收到的消息(玩家命令)放入一个队列
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; i++)
        {
            cout << "inMsgRecvQueue(),插入一个元素" << i << endl;

            {
                std::lock_guard<std::mutex> myGuard(myMutex);
                //myMutex.lock();              //加锁
                msgRecvQueue.push_back(i);   //假设i就是收到的命令,存入消息队列
               //myMutex.unlock();            //解锁
            }  //加个{}可以提前执行lock_guard的析构函数,即执行unlock()**********小技巧**********
            
        }
        return;
    }

    //判断消息是否为空,不为空取出命令
    bool outMsgProc(int& command)
    {
        std::lock_guard<std::mutex> mutexGuard(myMutex); //创建lock_guard对象
        //myMutex.lock();                                
        //加锁(使用了lock_guard,下面就可以不用使用lock和unlock)
        if (!msgRecvQueue.empty())
        {
            //消息不为空
            command = msgRecvQueue.front();             //返回第一个元素
            msgRecvQueue.pop_front();                   //移除第一个元素,但不返回
                                                        //处理取出的数据
                                                        //.............
            //myMutex.unlock();                         //消息不为空时解锁

            return true;
        }
        //myMutex.unlock();                             //消息为空时解锁
        return false;
    }

    //线程:把收到的命令取出
    void outMsgRecvQueue()
    {
        int command;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result)
            {
                cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
                //处理命令
            }
            else
            {
                //消息队列为空
                cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
            }
        }

        cout << "end" << endl;
    }
private:
    list<int> msgRecvQueue;      //存放玩家发送的命令队列(共享数据)
    mutex myMutex;               //创建了一个互斥量
};

int main(int argc, char** argv)
{
    A obj;

    std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);    
    //第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)

    std::thread myInMsgObj(&A::inMsgRecvQueue, &obj);       //第二个参数未引用

    myOutMsgObj.join();                                     //阻塞主线程并等待子线程执行完毕
    myInMsgObj.join();

    return 0;
}

该代码不会像上一篇的示例(不定时出现程序崩溃),而是一直正常运行,可自行测试

三、死锁(至少两把锁(互斥量)才会产生死锁)

一个互斥量相当于一把锁,如果有两把锁(锁1和锁2),并且有两个线程(线程A和线程B
1)线程A执行的时候先锁 锁1,即锁1.lock()并且成功,然后要去lock锁2
途中出现了上下文切换,线程B执行
2)线程B执行了,这个线程先锁 锁2,因为锁2还没有被锁,所以锁2会在此线程中lock()成功,然后线程B要去lock锁1...
此时死锁产生
3)线程A拿不到锁2,流程无法继续(所有后边的程序有解锁锁1的,但是流程执行不下去,所以锁1无法解开
4)线程B拿不到锁1,流程无法继续(所有后边的程序有解锁锁2的,但是流程执行不下去,所以锁2无法解开
大家都卡在这里,


3.1)下面为产生死锁的程序演示
用成员函数作为线程函数的方法来写线程

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


            myMutex1.lock();              //先加锁1
            // ....                       //执行其他操作
            myMutex2.lock();              //后加锁2
            msgRecvQueue.push_back(i);    //假设i就是收到的命令,存入消息队列
            myMutex2.unlock();            //先解锁2
            //.....                       //执行其他操作
            myMutex1.unlock();            //后解锁1

        }
        return;
    }

    //判断消息是否为空,不为空取出命令
    bool outMsgProc(int& command)
    {
        myMutex2.lock();                                //先加锁2
        myMutex1.lock();                                //后加锁1
        if (!msgRecvQueue.empty())
        {
            //消息不为空
            command = msgRecvQueue.front();             //返回第一个元素
            msgRecvQueue.pop_front();                   //移除第一个元素,但不返回
                                                        //处理取出的数据
                                                        //.............
            myMutex1.unlock();                         //先解锁1
            myMutex2.unlock();                         //后解锁2

            return true;
        }
        myMutex1.unlock();                             //先解锁1
        myMutex2.unlock();                             //后解锁1

        return false;
    }

    //线程:把收到的命令取出
    void outMsgRecvQueue()
    {
        int command;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result)
            {
                cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
                //处理命令
            }
            else
            {
                //消息队列为空
                cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
            }
        }

        cout << "end" << endl;
    }

private:
    list<int> msgRecvQueue;      //存放玩家发送的命令队列(共享数据)
    mutex myMutex1;              //创建了一个互斥量(锁1)
    mutex myMutex2;              //创建另外一个互斥量(锁2)
};

int main(int argc, char** argv)
{
    A obj;

    std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);    
    //第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)

    std::thread myInMsgObj(&A::inMsgRecvQueue, &obj);       //第二个参数未引用

    myOutMsgObj.join();                                     //阻塞主线程并等待子线程执行完毕
    myInMsgObj.join();

    return 0;
}

执行如上程序,可以发现程序不定时出现卡死现象,即死锁现象产生了,如下图所示



3.2)死锁的一般解决方案
只要保证两个线程中两个互斥量(锁)的上锁顺序一致就不会死锁
比如,可以将其中一个线程上锁顺序与另一个线程一致,这里,下面的程序先加锁1,再加锁2,代码如下。

bool outMsgProc(int& command)
    {
        myMutex1.lock();                                //先加锁1
        myMutex2.lock();                                //后加锁2
        if (!msgRecvQueue.empty())
        {
            //消息不为空
            command = msgRecvQueue.front();             //返回第一个元素
            msgRecvQueue.pop_front();                   //移除第一个元素,但不返回
                                                        //处理取出的数据
                                                        //.............
            myMutex1.unlock();                         //先解锁1
            myMutex2.unlock();                         //后解锁2

            return true;
        }
        myMutex1.unlock();                             //先解锁1
        myMutex2.unlock();                             //后解锁1

        return false;
    }

重新运行程序,发现不会出现死锁现象,即程序运行一致不会卡死。


3.3)std::lock()函数模板(用来处理多个互斥量
可以一次性锁住至少两个互斥量,而一个不行
不存在在多个线程中由于多个互斥量(锁)上锁的顺序问题而导致死锁现象产生
如果互斥量中有一个没锁住,就unlock已经锁住的,然后不断再次尝试lock所有的互斥量,等所有互斥量都锁住,它才往下走(返回)
比如该例子中,要么两个互斥量都锁住,要么两个互斥量都没锁住

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


            //myMutex1.lock();              //先加锁1
                                          // ....                       //执行其他操作
            //myMutex2.lock();              //后加锁2

            //改进:
            std::lock(myMutex1, myMutex2);//相当于两个互斥量同时lock

            msgRecvQueue.push_back(i);    //假设i就是收到的命令,存入消息队列
            myMutex2.unlock();            //先解锁2
                                          //.....                       //执行其他操作
            myMutex1.unlock();            //后解锁1

        }
        return;
    }

    //判断消息是否为空,不为空取出命令
    bool outMsgProc(int& command)
    {


        //myMutex2.lock();                                //先加锁2
        //myMutex1.lock();                                //后加锁1

        //改进
        std::lock(myMutex1, myMutex2);                  //相当于两个互斥量同时lock

        if (!msgRecvQueue.empty())
        {
            //消息不为空
            command = msgRecvQueue.front();             //返回第一个元素
            msgRecvQueue.pop_front();                   //移除第一个元素,但不返回
                                                        //处理取出的数据
                                                        //.............
            myMutex1.unlock();                         //先解锁1
            myMutex2.unlock();                         //后解锁2

            return true;
        }
        myMutex1.unlock();                             //先解锁1
        myMutex2.unlock();                             //后解锁1

        return false;
    }

    //线程:把收到的命令取出
    void outMsgRecvQueue()
    {
        int command;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result)
            {
                cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
                //处理命令
            }
            else
            {
                //消息队列为空
                cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
            }
        }

        cout << "end" << endl;
    }

private:
    list<int> msgRecvQueue;      //存放玩家发送的命令队列(共享数据)
    mutex myMutex1;              //创建了一个互斥量(锁1)
    mutex myMutex2;              //创建另外一个互斥量(锁2)
};

int main(int argc, char** argv)
{
    A obj;

    std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);     //第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)

    std::thread myInMsgObj(&A::inMsgRecvQueue, &obj);       //第二个参数未引用

    myOutMsgObj.join();                                     //阻塞主线程并等待子线程执行完毕
    myInMsgObj.join();

    return 0;
}

运行上面的程序,不会出现死锁。其实上面两个线程中只要有一个使用了std::lock()便不会出现死锁现象(上面两个线程中都使用了std::lock())。


3.4)避免忘记unlock
遗憾:使用std::lock()后还是要面临忘记unlock()的问题
解决:使用std::lock_guardstd::adopt_lock参数
std::adopt_lock是个结构体对象,起标记作用,表示互斥量已经lock(),不需要在std::lock_guard<std::mutex>里面对mutex对象进行lock().
std::lock()一次锁定多个互斥量谨慎使用(建议一个一个lock())

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

            //myMutex1.lock();              //先加锁1
            // ....                       //执行其他操作
            //myMutex2.lock();              //后加锁2

            //改进:
            std::lock(myMutex1, myMutex2);//相当于两个互斥量同时lock

            std::lock_guard<std::mutex> myGuard1(myMutex1, std::adopt_lock); //使用这个避免忘记unlock
            std::lock_guard<std::mutex> myGuard2(myMutex2, std::adopt_lock);

            msgRecvQueue.push_back(i);    //假设i就是收到的命令,存入消息队列

            //myMutex2.unlock();            //先解锁2  
            //执行其他操作
            //myMutex1.unlock();            //后解锁1

        }
        return;
    }

    //判断消息是否为空,不为空取出命令
    bool outMsgProc(int& command)
    {


        //myMutex2.lock();                                //先加锁2
        //myMutex1.lock();                                //后加锁1

        //改进:
        std::lock(myMutex1, myMutex2);                  //相当于两个互斥量同时lock

        std::lock_guard<std::mutex> myGuard1(myMutex1, std::adopt_lock);//使用这个避免忘记unlock
        std::lock_guard<std::mutex> myGuard2(myMutex2, std::adopt_lock);

        if (!msgRecvQueue.empty())
        {
            //消息不为空
            command = msgRecvQueue.front();             //返回第一个元素
            msgRecvQueue.pop_front();                   //移除第一个元素,但不返回
                                                        //处理取出的数据
                                                        //.............
            //myMutex1.unlock();                         //先解锁1  
            //上面使用了adopt_lock参数,此处不必unlock
            //myMutex2.unlock();                         //后解锁2

            return true;
        }
        //myMutex1.unlock();                             //先解锁1
        //myMutex2.unlock();                             //后解锁1

        return false;
    }
    //线程:把收到的命令取出
    void outMsgRecvQueue()
    {
        int command;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result)
            {
                cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;
                //处理命令
            }
            else
            {
                //消息队列为空
                cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << endl;
            }
        }
        cout << "end" << endl;
    }

private:
    list<int> msgRecvQueue;      //存放玩家发送的命令队列(共享数据)
    mutex myMutex1;              //创建了一个互斥量(锁1)
    mutex myMutex2;              //创建另外一个互斥量(锁2)
};

int main(int argc, char** argv)
{
    A obj;

    std::thread myOutMsgObj(&A::outMsgRecvQueue, &obj);     
    //第二个参数为引用,保证子线程操作的是主线程中的obj,不会复制(可看上一篇,第二篇博客)

    std::thread myInMsgObj(&A::inMsgRecvQueue, &obj);       //第二个参数未引用

    myOutMsgObj.join();                                     //阻塞主线程并等待子线程执行完毕
    myInMsgObj.join();

    return 0;
}

相关文章

网友评论

      本文标题:多线程学习(四)

      本文链接:https://www.haomeiwen.com/subject/ivwozltx.html