muduo网络库学习—跨线程调用及EventLoopThread(4)

muduo网络库学习—跨线程调用及EventLoopThread(4)



前言

在本章中主要介绍在muduo网络库中如何实现跨线程调用以及对IO线程的封装类EventLoopThread。


一、如何跨线程调用

不进行跨线程调用会怎么样:
由上一篇文章我们可以知道一个EventLoop对象有一个timerQueue队列,如果多个线程访问这个对象,那么这个对象属于临界资源,如果多个线程不加锁同时访问的话就会出现问题,但是加锁的话又会影响服务器的性能。所以在muduo中作者在runInLoop的方法实现了成员函数既可以跨线程调用又不用加锁,但是本质是上但是在一个loop所在的线程中执行的。

1.进程线程之间的通信

通信有四种方法:单向管道,双向管道,eventfd,条件变量
单向管道:pipe,pipe[0]只能用于读不能用于写,pipe[1]反之;
双向管道:soketpair既可以用于读又可以用于写
eventfd:是一个比pipe更高效的事件通知机制,一方面他比pipe少用一个文件描述符,另一方面他只有八个字节的固定缓冲区,不像pipe一样具有不定长的真实缓冲区,缓冲区管理简单多了。
条件变量:pthread_cond_signal,pthread_cond_wait等!(不适用于进程)

2.muduo中如何实现跨线程通信

思维导图:
在这里插入图片描述

在这里插入图片描述

在构造函数中实现wakeupfd(使用eventfd创建)的创建以及wakeupChannel的创建并加入到Poller中进行监听,设置成员函数handleRead为读回调函数。

代码如下:

Eventloop::Eventloop():looping_(false),
                        quit_(false),
                        eventHandling_(false),
                        thread_(CurrentThread::getTid()),
                        poller_(Poller::newDefaultPoller(this)),
                        currentChannel(NULL),
                        timerQueue_(new TimerQueue(this)),
                        wakeupFd_(creatEventFd()),
                        wakeupChannel_(new Channel(this,wakeupFd_))
{
    LOG_TRACE << "EventLoop created " << this << " in thread " << thread_;
    // 如果当前线程已经创建了EventLoop对象,终止(LOG_FATAL)
    if (t_loopInThisThread)
    {
        LOG_FATAL << "Another EventLoop " << t_loopInThisThread
                << " exists in this thread " << thread_;
    }
    else
    {
        t_loopInThisThread = this;
    }
    wakeupChannel_->setReadCallBack(std::bind(&Eventloop::handleRead,this));
    wakeupChannel_->enableReading();

}

设置唤醒的函数以及回调读函数,分别只是往文件描述符中写入一个size_t的变量以及将其读出来

代码如下:

// 该函数可以跨线程调用
void Eventloop::wakeup(){
    size_t n=1;
    size_t k=::write(wakeupFd_,&n,sizeof(n));
    assert(sizeof(n)==k);
}

void Eventloop::handleRead()
{
    uint64_t one = 1;
    //ssize_t n = sockets::read(wakeupFd_, &one, sizeof one);
    ssize_t n = ::read(wakeupFd_, &one, sizeof one);
    if (n != sizeof one)
    {
        LOG_ERROR << "EventLoop::handleRead() reads " << n << " bytes instead of 8";
    }
}

跨线程调用函数以及往目标线程中添加计算任务:runInLoop函数如果当前线程就是loop的所属线程则则直接调用函数,否则加入到计算任务队列中。添加的计算任务队列后,是否唤醒目标进程分三种情况:1.当前执行的线程不是目标线程则需要唤醒目标线程;2.当前线程就是目标线程但是正处于处理计算任务中则需要唤醒进程;3.在当前线程中且正在处理回调函数,不需要唤醒线程,因为处理完回调函数后紧接着就处理计算任务队列了(具体见思维导图中的loop函数进行理解)。
另外值得一提的是在前一章提到的EventLoop的成员函数runAt,runAfter等都是需要调用runinloop函数保证不加锁下的线程安全。EventLoop只把pendingFunctors暴露给其他线程,因此我们在向计算队列中加任务的时候要上锁。

代码如下:

// 在I/O线程中执行某个回调函数,该函数可以跨线程调用
void Eventloop::runInLoop(const Func&cb){
    if(isInLoopThread()){
        // 如果是当前IO线程调用runInLoop,则同步调用cb
        cb();
    }
    else{
        //如果不在创建loop的线程中则往该贤臣数组添加工作并且进行wakeup处理
        queueInLoop(cb);
    }
}

void Eventloop::queueInLoop(const Func&cb){
    //添加任务
    {
        MUtexLockGuard guard(mutex_);
        FuncVector.push_back(cb);
    }

    // 调用queueInLoop的线程不是IO线程需要唤醒
    // 或者调用queueInLoop的线程是IO线程,并且此时正在调用pending functor,需要唤醒
    // 只有IO线程的事件回调中调用queueInLoop才不需要唤醒
    if(!isInLoopThread()||callingPendingFunctors_){
        wakeup();
    }
}

对计算任务的处理,关键在于我们是直接将计算任务队列拷贝出来,这样减少的临界区,同时因为计算任务很可能再次调用了queueInLoop,则会造成死锁,还有一个原因是防止一直有源源不断的任务添加进来,这样IO线程陷入死循环,无法处理IO事件。
顺便解答一个作者在muduo书中提出的问题:为什么不在headEvent直接处理计算任务队列?这是因为如果我们是处于事件回调中调用queueInLoop的话,则省去了wakeup->poll->handread三次系统调用,可以直接处理任务队列。
还有Eventloop对象掌管来wakeupChannel的生命周期,这里我们使用unique_ptr智能指针来控制其生命周期。另外提一句上一节课timerQueue同时也管理着一个Channel对象的生命周期,但是不是用指针进行管理而是直接使用对象实体。

二、EventLoopThread类

思维导图:
在这里插入图片描述

1.简介

通过创建一个EventLoopdThread对象并且调用该对象的startloop函数(返回EventLoop指针)这样就创建了一个IO线程。startLoop内部创建一个线程,该线程创建一个Eventloop对象并执行事件循环。这里的回调函数cb是在执行事件循环前的初始化,如果没有设置则为空。

代码如下:

#ifndef EVENTLOOPTHREAD_H
#define EVENTLOOPTHREAD_H

#include "Eventloop.h"
#include "../base/Mutex.h"
#include "../base/Condition.h"
#include "../base/Thread.h"
#include <boost/function.hpp>
#include <boost/bind.hpp>
namespace muduo{
    namespace net{
        //该类主要用来封装一个IO线程
        class EventLoopThread{
        public:
        typedef std::function<void(Eventloop*)> ThreadInitCallback;

        EventLoopThread(const ThreadInitCallback &cb=ThreadInitCallback());
        ~EventLoopThread();
        
        Eventloop*startLoop();//启动线程,该线程就变成了IO线程

        private:
        void threadFunc();//线程函数
        Eventloop*loop_;//指向线程中创建的eventloop对象
        bool exiting_;//线程是否退出
        Thread thread_;//IO线程
        Mutex mutex_;//对应的锁以及条件变量
        Condition condition_;
        
        ThreadInitCallback callback_;
        
        };
    }
}
#endif

2.开启IO线程

代码如下(示例):

#include "EventLoopThread.h"
#include "../base/Logging.h"

namespace muduo{
    namespace net{
        EventLoopThread::EventLoopThread(const ThreadInitCallback&cb):
        exiting_(false),
        thread_(boost::bind(&EventLoopThread::threadFunc,this)),
        mutex_(),
        condition_(mutex_),
        callback_(cb),
        loop_(nullptr)
        {

        }

        EventLoopThread::~EventLoopThread(){
            assert(!exiting_);
            exiting_=true;
            loop_->quit();//退出循环
            thread_.join();//线程回收
        }

        Eventloop*EventLoopThread::startLoop(){

            thread_.start();
            {
                MUtexLockGuard guard(mutex_);
                while(!loop_){
                    condition_.wait();
                }
            }
            return loop_;
        }

        void EventLoopThread::threadFunc(){
            Eventloop loop;
            if(callback_){
                callback_(&loop);
            }

            {
                // loop_指针指向了一个栈上的对象,threadFunc函数退出之后,这个指针就失效了
                // threadFunc函数退出,就意味着线程退出了,EventLoopThread对象也就没有存在的价值了。
                // 因而不会有什么大的问题
                MUtexLockGuard guard(mutex_);
                loop_=&loop;
                condition_.notify();
            }

            loop_->loop();
        }
    }
}

在这里具体来说说条件变量的使用,首先条件变量一定是要配合着互斥量来使用的,总共有两次加锁两次解锁操作,以及配合着while的使用:

在这里插入图片描述第一次加锁:目的是queue.empty()到cond.wait()期间,条件变量不发生变化,如果不加锁,那么在queue.empty()到cond.wait()期间发生了cond.signal()但是cond.wait()是无法收到的,因此wait端只能等下一次signal到来。
第一次解锁:在进入cond.wait()后会解锁,这样的目的是使得其他线程改变条件变量来唤醒当前进程。
第二次加锁:在cond.wait()后解锁目的是在cond.wait()到while()判断期间条件量不会发生变化,因为跳出while循环后在解锁前默认条件变量是不变的,如果不加锁临界量被其他线程改变的话是容易出现错误的。
第二次解锁:释放锁使得其他进程得以推进

配合while使用:假设当前有A,B两个线程等待条件变量,C进行条件变量的唤醒,B首先被cond.wait()但是还没有抢占锁,A先抢占了锁然后判断条件符合进行出队操作,然后B等A释放锁之后在进行判断不符合条件只能解锁继续阻塞。

具体见两位大佬博客:
两次加锁两次解锁操作:见博客:https://blog.csdn.net/shichao1470/article/details/89856443?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165226010516781685385300%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=165226010516781685385300&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-1-89856443-null-null.142v9control,157v4control&utm_term=pthread_cond_wait%E5%8A%A0%E9%94%81&spm=1018.2226.3001.4187
配合着while:https://www.cnblogs.com/tqyysm/articles/9765667.html

配合while使用:https://www.cnblogs.com/tqyysm/articles/9765667.html


总结

本章在于在不加锁的情况下进行跨线程调用保证线程安全,同时条件变量的两次加锁解锁以及while的配合使用也是关键。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值