C++新特性(六)多线程(4)condition_variable(条件变量)、wait、notify_one、notify_all

文章目录

  • 一,条件变量的作用
  • 二,conditioin_variable;wait()
  • 三,conditioin_variable;notify_one()
    • 补充1——wait函数没有第二个参数可能会有的问题
    • 补充2——notify_all()函数。

一,条件变量的作用

学过操作系统的同学们应该知道,在进程管理那一章节,为了管理进程互斥进入临界区有了互斥信号量,这和我们线程里互斥访问共享数据思路差不多。但进程管理中同样有进程同步的管理,例如最经典的生产者消费者问题:缓冲区可容纳10个产品,因此生产者生产时必须先确定缓冲区有空位;消费者消费时必须确定缓冲区有产品。对于我们上一篇讲的场景来说——该场景模拟一个简单的游戏服务器,该服务器中有两个线程,一个线程负责往一个共享队列写玩家命令数据;另一个线程负责从这个共享队列中读取玩家命令并执行。共享队列的长度没有限制,所以玩家命令输入线程不需要管队列还能不能存命令;但是处理玩家命令线程就不同了,它应该先确认共享队列中有数据,有数据以后再进行处理。
要实现上述的功能,C++11多线程提供了condition_variable类,可以直译为条件变量,在该场景中,处理玩家命令线程等待条件(共享队列是否有数据)出现,而玩家命令输入线程在输入数据到共享队列以后就可以通知玩家命令处理线程。这是一个大致的过程,下面先介绍一下condition_variable类,然后结合代码具体演示一下。

二,conditioin_variable;wait()

std::condition_variable实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。
首先在类的私有变量里添加一个类对象;

std::condition_variable condition;//声明一个条件变量

然后在outMsgRecvQueue()中使用该条件变量,等待一个条件

void outMsgRecvQueue() {
        for (int i = 0; i < 4000; ++i) {
            std::unique_lock<std::mutex> sbguard(my_mutex);//先上锁
            condition.wait(sbguard, [this] {if (!msgRecvQueue.empty())//等待条件变量成立,成立条件为第二个参数返回值
                return true;                                        //也可以不带第二个参数,具体效果后面会说明
            return false;
                });
            int command = msgRecvQueue.front();// 返回第一个元素,不检查元素是否存在
            msgRecvQueue.pop_front();
            cout << "处理了" << command << "命令" << endl;
        }
        cout << "end " << endl;
    }

上面就是用条件变量更改了outMsgRecvQueue()线程以后的代码,主要是要理解这一句代码condition.wait(sbguard, [this] {if (!msgRecvQueue.empty())//等待条件变量成立,成立条件为第二个参数返回值 return true; //也可以不带第二个参数,具体效果后面会说明 return false; });
在这句代码中,主要理解第二个参数的返回值:
a)如果表达式为false,那wait就又放弃对my_mutex的锁,然后又休眠,等待再次被notify_one()唤醒,注意:如果没有另一个线程notify_one()它,那么它就会一直休眠在该行代码处。读者现在就可以用现在的outMsgRecvQueue()线程替换原来的outMsgRecvQueue()线程,观察运行结果,体会inMsgRecvQueue()线程没有对它notify_one时的效果。(可以在这条代码的lambda表达式里面的return false语句处打断点观察线程在这休眠的效果)只要执行到wait之后,那outMsgRecvQueue()线程是一定拿到了锁的。
b)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住)。
另外,wait也可以直接没有第二个参数,效果相当于有第二个参数返回false;

三,conditioin_variable;notify_one()

在该场景中,只需要在inMsgRecvQueue()线程中,当线程中网共享队列中输入数据后condition.notify_one()即可,该语句就会唤醒任意一个等待该条件的线程。

void inMsgRecvQueue() {
        for (int i = 0; i < 4000; ++i) {
            unique_lock<mutex> sbguard(my_mutex);
            cout << "inMsgRecvQueue()执行、插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);
            condition.notify_one();
         
        }
    }

最后贴上完整代码,注意我这里改了循环次数为4000次,方便观察最后的结果,最后发现inMsgRecvQueue()线程输入的4000条数据都会在outMsgRecvQueue()线程中被处理。这是我们以前的代码办不到的,因为以前的代码outMsgRecvQueue()线程在循环处理4000次不一定每次都能取到数据。

#include 
#include 
#include 
#include 
#include 
using namespace std;
//该代码让out线程中的unique_lock使用try_to_lock参数尝试拿锁
class A {
public:
    // 把收到的消息(玩家命令)放入到一个队列
    void inMsgRecvQueue() {
        for (int i = 0; i < 4000; ++i) {
            unique_lock<mutex> sbguard(my_mutex);
            cout << "inMsgRecvQueue()执行、插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);
            condition.notify_one();
         
        }
    }
    // 把数据从消息队列中取出的线程
    void outMsgRecvQueue() {
        for (int i = 0; i < 4000; ++i) {
            std::unique_lock<std::mutex> sbguard(my_mutex);//先上锁
            condition.wait(sbguard, [this] {if (!msgRecvQueue.empty())//等待条件变量成立,成立条件为第二个参数返回值
                return true;                                        //也可以不带第二个参数,具体效果后面会说明
            return false;
                });
            if (sbguard.owns_lock())//拿到锁头了
            {
              
                    int command = msgRecvQueue.front();// 返回第一个元素,不检查元素是否存在
                    msgRecvQueue.pop_front();
                    cout << "处理了" << command << "命令" << endl;
            }
            else//没有拿到锁头
            {
                cout << "没有拿到锁头,去干点别的事情" << endl;
            }
           
        }
        cout << "end " << endl;
    }
private:
    std::list<int> msgRecvQueue;
    std::mutex my_mutex;
    std::condition_variable condition;//声明一个条件变量
};
int main()
{
    A myobja;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
    myInMsgObj.join();
    myOutnMsgObj.join();
    return 0;
}

补充1——wait函数没有第二个参数可能会有的问题

如果wait函数没有第二个参数,也就是说不论如何都返回false,只有当有线程唤醒它一次,它才会向下执行一次。这样就会有一个问题:因为outMsgRecvQueue()与inMsgRecvQueue()并不是一对一执行的,所以当程序循环执行很多次以后,可能在msgRecvQueue 中已经有了很多消息,但是,outMsgRecvQueue还是被唤醒一次只处理一条数据。这时可以考虑把outMsgRecvQueue多执行几次,或者对inMsgRecvQueue进行限流。或者直接建议在wait函数中加入第二个参数,像前面介绍的一样,确保能得到每一个数据,这样更安全。(也就是说,我们设置条件变量的目的是等待什么条件,就把这个条件的lambda表达式写到第二个参数上)

补充2——notify_all()函数。

还是我们这个场景,有可能有多个线程来处理共享队列中存储的命令。
notify_one():因为只唤醒等待队列中的第一个线程;不存在锁争用,所以能够立即获得锁。其余的线程不会被唤醒,需要等待再次调用notify_one()或者notify_all()。
notify_all():会唤醒所有等待队列中阻塞的线程,存在锁争用,只有一个线程能够获得锁。那其余未获取锁的线程接着会怎么样?会阻塞?还是继续尝试获得锁?
答案是会继续尝试获得锁(类似于轮询),而不会再次阻塞。当持有锁的线程释放锁时,这些线程中的一个会获得锁。而其余的会接着尝试获得锁。注意,如果此时wait没有第二个参数,则notify_all以后,所有等待接收数据的线程都会依次获得锁去处理,导致可能有的线程不阻塞得到锁继续运行下去了,但是msg里可能没数据了。所以还是建议wait()有第二个参数,同时有多个线程的话可以notify_all()。
下一篇
C++新特性(六)多线程(5)async、future、packaged_task、shared_future、promise

你可能感兴趣的:(#,C++新特性,c++,开发语言,后端)