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

多线程学习(十一)

作者: lxr_ | 来源:发表于2021-11-29 17:04 被阅读0次

所需头文件

#include "stdafx.h"
#include <mutex>
#include <iostream>
#include <list>
using namespace std;

一.windows临界区

下面我们介绍一下,windows临界区,与我们前面介绍的C++中的mutex&非常类似,依然利用多线程学习(四)中的示例进行演示:包括了读和写线程
该示例说明如下:
假设做一个简易的网络游戏服务器:
两个自己创建的线程,一个线程收集玩家命令(用数字代表),并将命令写到一个队列中
另一个线程从队列中取出玩家发送来的命令,解析,然后执行玩家需要的动作

首先,需要额外包含头文件

#include <Windows.h>
#define __WINDOWSJQ_

线程入口函数
代码逻辑为如果打开了使用windows临界区的开关,则在读写线程中分别使用windows临界区进行共享数据保护,否则使用C++中的mutex,如下:

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

#ifdef __WINDOWSJQ_                               //如果打开了使用windows临界区的开关
            EnterCriticalSection(&myWinSec);      //进入临界区(加锁)
            msgRecvQueue.push_back(i);            //假设数字i为收到的命令
            LeaveCriticalSection(&myWinSec);      //退出临界区(解锁)
#else                                             //否则使用mutex
            myMutex.lock();
            msgRecvQueue.push_back(i);            //假设数字i为收到的命令
            myMutex.unlock();

#endif // __WINDOWSJQ_
        }
    }

    bool outMsgProc(int& command)
    {
#ifdef __WINDOWSJQ_
        EnterCriticalSection(&myWinSec);
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front();      //返回第一个元素但不检查元素是否存在
            msgRecvQueue.pop_front();
            LeaveCriticalSection(&myWinSec);
            return true;
        }
        LeaveCriticalSection(&myWinSec);
#else
        myMutex.lock();
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front();      //返回第一个元素但不检查元素是否存在
            msgRecvQueue.pop_front();
            myMutex.unlock();
            return true;
        }
        myMutex.unlock();
#endif // __WINDOWSJQ_
        return false;
    }

    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result == true)
            {
                cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
            }
            else
            {
                cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
            }
        }
        cout << "end" << endl;
    }
    A()
    {
#ifdef __WINDOWSJQ_

        InitializeCriticalSection(&myWinSec);        //用临界区之前要先初始化
#endif // __WINDOWSJQ_

    }

private:
    std::mutex myMutex;            //代表玩家发送来的命令容器
    std::list<int> msgRecvQueue;   //互斥量

#ifdef __WINDOWSJQ_
    CRITICAL_SECTION myWinSec;     //windows中的临界区,类似于c++中的mutex
#endif                        
};

主函数

int main()
{
    A myObj;
    std::thread myOutMsgObj(&A::outMsgRecvQueue, &myObj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myObj);

    myOutMsgObj.join();
    myInMsgObj.join();

    return 0;

    return 0;
}

运行上述程序,因为默认打开了使用windows临界区的开关,即语句#define _WINDOWSJQ,类似使用mutex一样,程序稳定运行。

二.多次进入临界区实验

同一个线程中(不同线程就会卡住等待),windows中的“相同临界区变量”代表的临界区的进入(EnterCriticalSection)可以被多次调用
但是调用了几次EnterCriticalSection,就得调用几次LeaveCriticalSection
修改其中一个线程入口函数进行测试,如下:

//把收到的消息入到队列的线程
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; i++)
        {
            cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;

#ifdef __WINDOWSJQ_
            EnterCriticalSection(&myWinSec);      //两次进入临界区
            EnterCriticalSection(&myWinSec);

            msgRecvQueue.push_back(i);            //假设数字i为收到的命令

            LeaveCriticalSection(&myWinSec);      //退出临界区
            LeaveCriticalSection(&myWinSec);

#else
            myMutex.lock();
            msgRecvQueue.push_back(i);            //假设数字i为收到的命令
            myMutex.unlock();

#endif // __WINDOWSJQ_
        }
    }

那么C++允许多次加锁吗???答案是不允许的,会报异常(可以自行测试)
测试此种情况时,需要注释掉前面打开的#define __WINDOWSJQ_语句,并修改其中一个线程入口函数进行测试,如下:

//把收到的消息入到队列的线程
        void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; i++)
        {
            cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;

#ifdef __WINDOWSJQ_
            EnterCriticalSection(&myWinSec);

            msgRecvQueue.push_back(i);            //假设数字i为收到的命令

            LeaveCriticalSection(&myWinSec);

#else
            myMutex.lock();                       //多次加锁
            myMutex.lock();
            msgRecvQueue.push_back(i);            //假设数字i为收到的命令
            myMutex.unlock();
            myMutex.unlock();

#endif // __WINDOWSJQ_
        }
    }

三.自动析构技术

前面多线程学习(四)已经介绍过std::lock_guard<std::mutex>,那么允许多次lock_guard吗???
我们根据多线程学习(四)知道,使用lock_guard就可以不用lock和unlock,替代了lock和unlock,可以避免忘记lock_guard,所以与lock和unlock类似,也不能多次lock_guard
与上面测试是否能多次lock类似,先注释掉前面打开的 #define __WINDOWSJQ_ 语句,修改其中一个线程入口函数如下:

     //把收到的消息入到队列的线程
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; i++)
        {
            cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;

#ifdef __WINDOWSJQ_
            EnterCriticalSection(&myWinSec);

            msgRecvQueue.push_back(i);            //假设数字i为收到的命令

            LeaveCriticalSection(&myWinSec);

#else
            std::lock_guard<std::mutex> myLockGuard1(myMutex);
            std::lock_guard<std::mutex> myLockGuard2(myMutex);

            msgRecvQueue.push_back(i);            //假设数字i为收到的命令

#endif // __WINDOWSJQ_
        }
    }

修改后,运行程序发现程序报出异常,不能多次lock_guard
还有个问题,windows下有没有类似lock_guard的模板类,也就是避免忘记LeaveCriticalSection的方法呢?
不知道有没有,但我们可以自己实现,如下:
本类用于自动释放windows下的临界区,方式忘记LeaveCriticalSection,导致死锁情况发生,类似于C++中的std::lock_guard<std::mutex>
这种类叫做叫RAII类(Resource Acquisition is initialization),即“资源获取即初始化”
容器,智能指针这种类都属于RAII类

class CWinLock
{
public:
    CWinLock(CRITICAL_SECTION* pWinSec)     //构造函数
    {
        myWinSec = pWinSec;
        EnterCriticalSection(myWinSec);     //进入临界区
    }
    ~CWinLock()                             //析构函数
    {
        LeaveCriticalSection(myWinSec);     //退出临界区
    }

private:
    CRITICAL_SECTION* myWinSec;

};

下面在程序中测试是否可以正常使用,如下:
测试其中一个线程入口函数即可,记得取消注释#define __WINDOWSJQ_语句

//把收到的消息入到队列的线程
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; i++)
        {
            cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;

#ifdef __WINDOWSJQ_
            //EnterCriticalSection(&myWinSec);
            //EnterCriticalSection(&myWinSec);
            CWinLock CWL1(&myWinSec);             //创建CWinLock对象,也RAII对象
            CWinLock CWL2(&myWinSec);             //调用多次也可以

            msgRecvQueue.push_back(i);            //假设数字i为收到的命令

            //LeaveCriticalSection(&myWinSec);
            //LeaveCriticalSection(&myWinSec);

#else
            std::lock_guard<std::mutex> myLockGuard1(myMutex);

            //myMutex.lock();
            msgRecvQueue.push_back(i);            //假设数字i为收到的命令
            //myMutex.unlock();

#endif // __WINDOWSJQ_
        }
    }

修改后运行程序,程序运行正常,且可以多次调用,这样就避免了忘记退出临界区的操作LeaveCriticalSection

四.recursive_mutex递归的独占互斥量

std::mutex:独占互斥量,自己lock时,别人lock不了
recursive_mutex:递归的独占互斥量,允许同一个线程,同一个互斥量多次lock
与mutex一样,也有lock和unlock
首先有个问题,在什么情况下,我们需要多次lock呢?
下面演示一下这种情况:
在类A中创建两个测试函数testFunc,这两个函数中调用了lock_guard,并在其中一个线程入口函数inMsgRecvQueue中调用testFunc1,则一共调用了三次lock,程序运行崩溃。

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

            std::lock_guard<std::mutex> myLockGuard1(myMutex);

            testFunc1();                          //调用此函数后,一共lock三次,程序崩溃

            //myMutex.lock();
            msgRecvQueue.push_back(i);            //假设数字i为收到的命令
            //myMutex.unlock();
        }
    }

    bool outMsgProc(int& command)
    {
        myMutex.lock();
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front();      //返回第一个元素但不检查元素是否存在
            msgRecvQueue.pop_front();
            myMutex.unlock();
            return true;
        }
        myMutex.unlock();
        return false;
    }

    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result == true)
            {
                cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
            }
            else
            {
                cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
            }
        }
        cout << "end" << endl;
    }

    void testFunc1()
    {
        std::lock_guard<std::mutex> myLockGuard1(myMutex);
        //...执行其他程序
        testFunc2();                                      //这个函数里还会加锁,崩溃
    }
    void testFunc2()
    {
        std::lock_guard<std::mutex> myLockGuard2(myMutex);
        //...执行其他程序
    }
private:
    std::mutex myMutex;            //代表玩家发送来的命令容器
    std::list<int> msgRecvQueue;   //互斥量                    
};

recursive_mutex与mutex用法类似,但可以解决多次lock问题
下面进行recursive_mutex的用法演示:修改类A中lock_guard模板类型为recursive_mutex

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

            std::lock_guard<std::recursive_mutex> myLockGuard1(myMutex);

            testFunc1();                          //调用此函数后,一共lock三次,

            msgRecvQueue.push_back(i);            //假设数字i为收到的命令
        }
    }

    bool outMsgProc(int& command)
    {
        myMutex.lock();
        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front();      //返回第一个元素但不检查元素是否存在
            msgRecvQueue.pop_front();
            myMutex.unlock();
            return true;
        }
        myMutex.unlock();
        return false;
    }

    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result == true)
            {
                cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
            }
            else
            {
                cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
            }
        }
        cout << "end" << endl;
    }

    void testFunc1()
    {
        std::lock_guard<std::recursive_mutex> myLockGuard1(recursive_mutex);
        //...执行其他程序
        testFunc2();                                      //这个函数里还会加锁,崩溃
    }
    void testFunc2()
    {
        std::lock_guard<std::recursive_mutex> myLockGuard2(recursive_mutex);
        //...执行其他程序
    }
private:
    std::recursive_mutex myMutex;            //互斥量
    std::list<int> msgRecvQueue;   //代表玩家发送来的命令容器
                      
};

修改后运行程序,程序运行正常,则recursive_mutex可以解决多次lock的问题
虽然recursive_mutex解决了多次lock的问题,但是代码是否有优化空间呢?
recursive_mutex的效率比mutex效率差一些,据说递归次数有限制,递归太多次可能报异常,可自行测试。

五.带超时的互斥量std::timed_mutex和std::recursive_timed_mutex

std::timed_mutex:带超时功能的独占互斥量
std::recursive_timed_mutex:带超时功能的递归独占互斥量
try_lock_for():参数是一段时间,等待一段时间,如果拿到锁(lock成功),执行正常拿到锁的程序,如果等待超过时间没拿到锁,就继续处理其他程序
try_lock_until()参数是一个未来的时间点,在未来的时间点没到的时间内,如果拿到锁,就正常执行拿到锁后的程序,没拿到锁处理其他程序
下面先测试try_lock_for函数的使用:同样修改类A中的代码如下:

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

            std::chrono::milliseconds timeout(100);//100ms
            if (myMutex.try_lock_for(timeout))     //等待100ms尝试获取锁
            {
                //在这100ms之内拿到锁
                msgRecvQueue.push_back(i);            //假设数字i为收到的命令

                myMutex.unlock();                     //用完要解锁
            }
            else                                      //让另一个线程lock后先停止一会,也就是先不unlock,可测试此种情况
            {
                //没拿到锁,程序停一会
                std::chrono::milliseconds sleeptime(100);//100ms
                std::this_thread::sleep_for(sleeptime);
                cout << "lock失败" << endl;
            }                                                                    
        }
    }

    bool outMsgProc(int& command)
    {
        myMutex.lock();
        std::chrono::milliseconds sleeptime(1000);//1s  测试拿不到锁的情况
        std::this_thread::sleep_for(sleeptime);

        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front();      //返回第一个元素但不检查元素是否存在
            msgRecvQueue.pop_front();
            myMutex.unlock();
            return true;
        }
        myMutex.unlock();
        return false;
    }

    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result == true)
            {
                cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
            }
            else
            {
                cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
            }
        }
        cout << "end" << endl;
    }

private:
    std::timed_mutex myMutex;      //带超时功能的独占互斥量
    std::list<int> msgRecvQueue;   //代表玩家发送来的命令容器
};

修改后运行程序,可以看到有获取不到锁的情况,但程序没有卡住,依然继续处理其他程序
下面演示try_lock_until的用法:与try_lock_for类似,只是参数不一样,可实现同样的功能

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

            std::chrono::milliseconds tiemout(100);     //100ms
            if (myMutex.try_lock_until(chrono::steady_clock::now()+tiemout)) //最多等待100ms,参数为当前时间+100ms
            {
                //在这100ms之内拿到锁
                msgRecvQueue.push_back(i);            //假设数字i为收到的命令

                myMutex.unlock();                     //用完要解锁
            }
            else                                      //让另一个线程lock后先停止一会,也就是先不unlock,可测试此种情况
            {
                //没拿到锁,程序停一会
                std::chrono::milliseconds sleeptime(100);//100ms
                std::this_thread::sleep_for(sleeptime);
                cout << "lock失败" << endl;

            }

        }
    }

    bool outMsgProc(int& command)
    {
        myMutex.lock();
        std::chrono::milliseconds sleeptime(1000);//1s  测试拿不到锁的情况
        std::this_thread::sleep_for(sleeptime);

        if (!msgRecvQueue.empty())
        {
            command = msgRecvQueue.front();      //返回第一个元素但不检查元素是否存在
            msgRecvQueue.pop_front();
            myMutex.unlock();
            return true;
        }
        myMutex.unlock();
        return false;
    }

    void outMsgRecvQueue()
    {
        int command = 0;
        for (int i = 0; i < 100000; i++)
        {
            bool result = outMsgProc(command);
            if (result == true)
            {
                cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
            }
            else
            {
                cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
            }
        }
        cout << "end" << endl;
    }

private:
    std::timed_mutex myMutex;      //带超时功能的独占互斥量
    std::list<int> msgRecvQueue;   //代表玩家发送来的命令容器
};

std::recursive_timed_mutex的用法这里就不演示了,比std::timed_mutex多了个多次lock的功能,相信大家也已经知道怎么使用了。

相关文章

网友评论

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

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