多线程学习(七)

2021-11-21  本文已影响0人  lxr_

问题:使用上一篇讲得双重锁定(双重检查)可以提高效率,避免先创建锁再判断而引起的效率低下问题
但是不断地判断if条件是否满足,也造成了一定的效率问题
解决

一:条件变量std::conidtion_variable,wait(),notify_one()

解释:有两个线程A和线程B,线程A等待一个条件满足(通知)才继续执行,线程B专门向内存中写入数据,并发布通知
std::condition_variable是一个和条件相关的类作用:等待一个条件达成(等到通知),需要和互斥量配合使用**
使用的时候生成这个类的对象

继续使用前面的例子进行测试,即包含两个线程(一个写线程和一个读线程)

#include "stdafx.h"
#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(),插入一个元素" << i << endl;

            std::unique_lock<std::mutex> myUniqueLock(myMutex); //使用unique_lock替代lock_guard

            msgRecvQueue.push_back(i);    //假设i就是收到的命令,存入消息队列
            myCond.notify_one();         
            //尝试把wait()的线程唤醒,执行完这一行后
            //outMsgRecvQueue线程里的wait()就会被唤醒,唤醒之后的事情后续说明.....

        }
        return;
    }

    //读线程:把收到的命令取出
    void outMsgRecvQueue()
    {
        int command;
        while (true)
        {
            std::unique_lock<std::mutex> myUniqueLock(myMutex);

            //wait()用来等待某一个消息
            //如果第二个参数lambda表达式返回值为true,wait()直接返回,程序向下执行
            //如果第二个参数的lambda表达式返回值为false,那么wait()将解锁互斥量,并堵塞到本行
            //堵塞到其他某个线程调用notify_one()成员函数为止
            //如果wait()无第二个参数,即为myCond.wait(myUniqueLock),
            //此情况那么就和第二个参数lambda表达式返回false的效果一样
            //也就是wait()将解锁互斥量,并堵塞到本行,并堵塞到其他某个线程调用notify_one()成员函数为止
            //当其他线程使用notify_one()将此wait()(原来为堵塞状态)唤醒后,wait()恢复执行,如下:
            //a)wait()不断尝试重新获取互斥量(锁),如果获取不到,那么流程还是卡在wait()处等着获取互斥量(锁)
            //如果获取到锁(加锁),wait()就继续执行下一步b
            //b)
            //b.1)如果wait有第二个参数lambda表达式,判断lambda表达式返回false
            //则wait()又对互斥量解锁,然后又堵塞到此处等待再次被notify_one唤醒
            //b.2)如果lambda表达式为true,则wait()返回,流程继续执行(此时互斥锁被锁着)
            //b.3)如果wait()没有第二个参数,则wait()返回,流程继续执行
            myCond.wait(myUniqueLock, 
                [this] {                            //一个lambda表达式就是一个可调用对象(函数)
                if (!msgRecvQueue.empty())
                    return true;
                return false;
            });

            //程序只要能执行到此处,互斥量一定是锁着的,且msgRecvQueue至少有一条数据的
            command = msgRecvQueue.front();       //返回第一个元素
            msgRecvQueue.pop_front();              //移除第一个元素,但不返回
            myUniqueLock.unlock();                 
            //因为unique_lock的灵活性,提前解锁后(以免锁住太长时间),写线程可以执行了
            cout << "outMsgRecvQueue()执行,取出一个命令" << command << endl;

        }
    }

private:
    list<int> msgRecvQueue;      //存放玩家发送的命令队列(共享数据)
    mutex myMutex;              //创建了一个互斥量
    std::condition_variable myCond;//创建一个条件变量对象
};
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.如果写线程inMsgRecvQueue先执行完后,而读线程outMsgRecvQueue还没执行完,则写线程不能执行notify_one通知读线程读线程就会一直卡在wait()函数处。
2.如果读线程读到数据后,还要执行一系列其他程序,耗时较长,而写线程又再一次执行到notify_one通知读线程,而读线程并没有卡在wait()处,而是在执行其他程序,那么notify_one就可能没有效果

二.notify_all()

notify_one()只能通知一个线程,使得一个线程被唤醒
当我们创建多个读线程outMsgRecvQueue时,如果仍使用notify_one,只能唤醒其中一个线程
所以怎么同时唤醒多个线程呢?使用notify_all()
虽然两个线程都被唤醒了,但是在wait()函数中,两个线程都要尝试获取锁,但是只有一个能获取到锁未获取到锁的线程还是得卡在wait()函数中
在下面的测试中,再创建一个读线程,使用notify_one和notify_all都是一样的效果,都只有一个线程继续执行

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

            std::unique_lock<std::mutex> myUniqueLock(myMutex); //使用unique_lock替代lock_guard

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

            //myCond.notify_one();
            //尝试把wait()的线程唤醒,执行完这一行后,outMsgRecvQueue线程里的wait()就会被唤醒.

            myCond.notify_all();       //尝试唤醒所有wait()的线程

        }
        return;
    }

    //读线程:把收到的命令取出
    void outMsgRecvQueue()
    {
        int command;
        while (true)
        {
            std::unique_lock<std::mutex> myUniqueLock(myMutex);

            //wait()用来等待某一个消息
            //如果第二个参数lambda表达式返回值为true,wait()直接返回
            //如果第二个参数的lambda表达式返回值为false,那么wait()将解锁互斥量,并堵塞到本行
            //堵塞到其他某个线程调用notify_one()成员函数为止
            //如果wait()无第二个参数,即myCond.wait(myUniqueLock),那么就第二个参数lambda表达式返回false的效果一样
            //也就是wait()将解锁互斥量,并堵塞到本行,并堵塞到其他某个线程调用notify_one()成员函数为止
            //当其他线程使用notify_one()将此wait()(原来为堵塞状态)唤醒后,wait()恢复执行
            //a)wait()不断尝试重新获取互斥量(锁),如果获取不到,那么流程还是卡在wait()处等着获取互斥量(锁)
            //如果获取到锁(加锁),wait()就继续执行下一步b
            //b)
            //b.1)如果wait有第二个参数lambda表达式,如果判断lambda表达式返回false
            //wait()又对互斥量解锁,然后又堵塞到此处等待再次被notify_one唤醒
            //b.2)如果lambda表达式为true,则wait()返回,流程继续执行(此时互斥锁被锁着)
            //b.3)如果wait()没有第二个参数,则wait()返回,流程继续执行
            myCond.wait(myUniqueLock,
                [this] {                            //一个lambda表达式就是一个可调用对象(函数)
                if (!msgRecvQueue.empty())
                    return true;
                return false;
            });

            //程序只要能执行到此处,互斥量一定是锁着的,且msgRecvQueue至少有一条数据的
            command = msgRecvQueue.front();       //返回第一个元素
            msgRecvQueue.pop_front();              //移除第一个元素,但不返回
            cout << "outMsgRecvQueue()执行,取出一个命令" << command
                 << "threadid=" << std::this_thread::get_id() << endl;
            myUniqueLock.unlock();                 
            //因为unique_lock的灵活性,提前解锁后(以免锁住太长时间),写线程可以执行了
            

        }
    }

private:
    list<int> msgRecvQueue;      //存放玩家发送的命令队列(共享数据)
    mutex myMutex;              //创建了一个互斥量
    std::condition_variable myCond;//创建一个条件变量对象
};
int main(int argc, char** argv)
{
    A obj;

    std::thread myOutMsgObj1(&A::outMsgRecvQueue, &obj);
                                                            //第二个参数为引用,保证子线程操作的是主线程中的obj
    std::thread myOutMsgObj2(&A::outMsgRecvQueue, &obj);    //再创建一个读线程

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

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

    return 0;
}
上一篇下一篇

猜你喜欢

热点阅读