muduo库EventLoop类源码解析

目录

引言

EventLoop的工作流程

EventLoop的数据结构

        绑定唯一的线程?    

        任务队列中有任务了?

        多路转接?

EventLoop最核心的接口

       捞取就绪事件?

        执行就绪事件的回调?

        执行任务队列中的任务会死锁?

EventLoop保证线程安全的接口

        判断线程ID

        把任务抛入到任务队列,死锁?

        唤醒IO线程的条件?

        执行任务队列的接口

EventLoop终止事件循环


引言

        本文适合于了解多路转接,了解muduo库的读者或想要看muduo库源码的读者,本文会把EventLoop类的核心源码拎出来解读。

        博主的主页链接:东洛的克莱斯韦克-优快云博客

        一篇关于多路转接的文章:高并发服务的核心机制——IO多路转接--->epoll-优快云博客

EventLoop的工作流程

        EventLoop也称为事件循环,从调用epoll_wait系统调用捞取到就绪事件到再次阻塞到epoll_wait上(阻塞到内核中的就绪队列上了)称为一次事件循环。muduo库的EventLoop在捞取完就绪事件后会立即执行就绪事件的回调。

        EventLoop还有一个线程安全的任务队列。在执行完就绪事件的回调后不会立即阻塞到epoll_wait上,他会在执行完任务队列中的任务后在阻塞到就绪队列上。

        为什么要搞一个线程安全的任务队列呢?muduo库的设计思想是One Loop One Thread,意思是一个事件循环绑定唯一的一个线程。用于监听并获取外部链接的One Loop One Thread称为主线程,用于和客户端通信的One Loop One Thread称为从线程或IO线程。

        单进程的情况下称为单Reactor模型,多线程的情况下称为主从Reactor模型。muduo库允许其他线程给IO线程派发任务,而且,为了保证执行就绪事件中任务是一定是和当前Loop绑定IO Thread,那么具有线程安全的任务队列就是必须的。

EventLoop的数据结构

下面是EventLoop源码定义的数据,我加了一些注释。

namespace muduo
{
namespace net
{
typedef std::function<void()> Functor;

class Channel;
class Poller;
class TimerQueue;

///
/// Reactor, at most one per thread.
///
/// This is an interface class, so don't expose too much details.
class EventLoop : noncopyable
{ 
private:
 typedef std::vector<Channel*> ChannelList;

  bool looping_;                  //为true表示正在运行loop()进行事件监控/* atomic */
  std::atomic<bool> quit_;        /*为false进行事件循环,true结束事件循环,quit_的赋值都是原子性的*/
  bool eventHandling_;            //为true表示正在执行就绪事件的回调/* atomic */
  bool callingPendingFunctors_;   //为true表示正在执行任务队列中的任务 /* atomic */
  int64_t iteration_;             //事件循环的次数
  const pid_t threadId_;          //绑定的线程ID
  Timestamp pollReturnTime_;
  std::unique_ptr<Poller> poller_;          //事件的监控,增加,删除
  std::unique_ptr<TimerQueue> timerQueue_;  //时间轮
  int wakeupFd_;                            //用于通知就绪队列的任务到来了
  // unlike in TimerQueue, which is an internal class,
  // we don't expose Channel to client.
  std::unique_ptr<Channel> wakeupChannel_;  //管理wakeupFd_文件描述符的事件
  boost::any context_;  //上下文

  // scratch variables
  ChannelList activeChannels_;    //捞取就绪事件的容器
  Channel* currentActiveChannel_; //当前正在活跃的事件

  mutable MutexLock mutex_;                                   //锁
  std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);   /*任务队列,加锁处理,是具有线程安全的*/
}
}
}

        绑定唯一的线程?    

         这个类是noncopyable(不可拷贝的),在muduo库中,大部分的类都是noncopyable的。threadId_是当前loop绑定的线程ID,要如何保证唯一的Loop帮定到唯一的Thread上?这其实涉及到EventLoopThread类了,我后面也会出一篇关于这个类的源码解析,大概原理是,在线程创建的时候创建Loop,这时直接初始化threadId_,这样就绑定到了一个唯一的线程了,但我们还需要一些同步原语来保证One Loop One Thread的设计思想,比如说这个pendingFunctors_(任务队列)。

        任务队列中有任务了?

        接下来要介绍的是wakeupFd_ 这个文件描述符。翻看构造函数不难发现这个文件描述符是用::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)初始化的。wakeupFd_ 会被当前的Loop进行事件监控,它的写事件回调是EventLoop::wakeup(),他会向内核的计数器上写入大小为8字节的1。

  uint64_t one = 1;
  ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);

        读事件回调是EventLoop::handleRead()。他会把内核中计数器上8字节的数据读取出来。

  uint64_t one = 1;
  ssize_t n = sockets::read(wakeupFd_, &one, sizeof one);

        wakeupFd_ 是用来通知当前线程有任务被抛入到了任务队列中,赶紧去执行吧。具体的细节是,当有线程向任务队列中抛入任务后,就会调用EventLoop::wakeup(),那么当前的IO线程会捞取到wakeupFd_ 的读事件以就绪。会立刻从epoll_wait系统调用上醒来。去执行wakeupFd_ 读回调,然后执行任务队列中的任务(参考前文所述的Loop的执行流程)。

        这样子显然是为了提高效率,在调用epoll_wait系统调用的时候,如果没有就绪事件,当前的IO线程就阻塞到了内核的就绪队列上,最长会阻塞const int kPollTimeMs = 10000,也就是10000毫秒才会返回。

        很显然wakeupFd_ 就是为了让IO线程捞取到就绪事件立马返回。那么还有一个问题,哪些时候需要调用EventLoop::wakeup()通知IO线程有任务了?这里先挖个坑,后文会详细解析。

        多路转接?

        上面的数据有几个类可能会比较陌生,其中pollReturnTime_和timerQueue_,和Event Loop不是强相关的(不会影响我们理解Event Loop),他们和定时任务有关,后面有机会出一篇关于定时任务的源码解析。这里我要重点说明的是poller_ 类。

        poller_ 其实是基类,poller_ 的方法用多态的方式实现了两份,一份是epoll系列的系统调用(EPollPoller类),一份是poll系列的系统调用(PollPoller类),他们都是多路转接的系统调用,可以用man手册查看具体的使用,在muduo源码中位于muduo::net::poerr目录中。

        以EPollPoller类为例,他其实就是封装了下面三个系统调用

        1.调用epoll_create创建一个epoll句柄;
        2.调用epoll_ctl, 将要监控的文件描述符进行注册;
        3.调用epoll_wait, 等待文件描述符就绪;
        

        所以poller_ 这个类就是实际和内核打交道的,用于实现多路转接的一个类。

EventLoop最核心的接口

        EventLoop::loop()是EventLoop最核心的接口,他是共有成员函数。功能是死循环进行事件监控。下面是伪代码,梳理一下loop()接口的逻辑。

void EventLoop::loop() {
    //一次while循环就表示一次事件循环
    while (1) {
        epoll_wait();    //捞取就绪事件

        for () { //执行就绪事件的回调 }

        doPendingFunctors()    //处理任务队列中的任务
    }
}

源码中用布尔值quit_作为循环的判断标志。当有线程把quit_改为true时,当前IO线程就结束事件循环,源码的循环如下,带下划线 _ 结尾的都是类内成员数据,前文已经注释了其含义。

  while (!quit_)
  {
    //每次捞取事件之前,先把容器清理干净,确保每次都是新事件
    activeChannels_.clear();
    //去epoll模型中的就绪队列捞取就去事件,该IO线程可能长时间的阻塞在这里
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_); //两个参数分别是最长等待时间和就绪事件的容器
    ++iteration_;
    if (Logger::logLevel() <= Logger::TRACE)
    {
      printActiveChannels();
    }
    // TODO sort channel by priority
    // 执行就绪事件的回调
    eventHandling_ = true;
    for (Channel* channel : activeChannels_)
    {
      currentActiveChannel_ = channel;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    currentActiveChannel_ = NULL;
    eventHandling_ = false;
    //在捞取的就绪事件处理完之后执行任务队列中的任务
    doPendingFunctors();
  }

       捞取就绪事件?

        poller_->poll(kPollTimeMs, &activeChannels_);就是在捞取就绪的事件。它会调用EPollPoller::poll或PollPoller::poll。前者会调用epoll_wait系统调用,后者会调用poll系统调用 。这两个系统调用都会去捞取已经就绪的事件,如果没有就绪的事件就会阻塞,最长阻塞10000毫秒,然后返回。

        前文已经阐述了wakeupFd_,如果没有IO事件,当前IO线程也不一定会阻塞,也有可能会因为有任务被抛入到任务队列中而被唤醒。

        执行就绪事件的回调?

        为了更好的理解上述代码,这里要介绍一个Channel 类,这个类封装了文件描述符,以及文件描述符要关心的事件,文件描述符已经就绪的事件,文件描述符不同事件的回调。以及一个核心的成员函数,即Channel::handleEvent,这个成员函数会根据不同的就绪事件调用不同的回调,让上层无需再判断哪种事件就绪了。上述代码中

        currentActiveChannel_->handleEvent(pollReturnTime_);就是在根据不同的就绪事件调用不同的回调函数。Channel::handleEvent的源码实现逻辑从略,就是判断已就绪的事件标志位,再根据不同事件调用不同的回调。

        Channel部分成员数据如下

  EventLoop* loop_;     //挂接到EventLoop上
  const int  fd_;       //监控的文件描述符
  int        events_;   //用户要关心的事件,由用户设置
  int        revents_;  //文件描述符已经触发了的事件

   Channel有4个回调,由set_ _ _ Callback()系列成员函数设置。

  typedef std::function<void()> EventCallback;
  typedef std::function<void(Timestamp)> ReadEventCallback;
  ReadEventCallback readCallback_;  //事件的读回调
  EventCallback writeCallback_;     //事件的写回调
  EventCallback closeCallback_;     //事件的关闭回调
  EventCallback errorCallback_;     //事件的错误回调

        muduo库在关心读事件时,允许紧急数据(带外数据),给内核设置的标志位是POLLIN | POLLPRI ,几个中要的成员函数如下

    const int Channel::kNoneEvent = 0;                //0
    const int Channel::kReadEvent = POLLIN | POLLPRI; //读事件,允许读取紧急数据
    const int Channel::kWriteEvent = POLLOUT;         //写事件  

  //要关心的事件是否为0(是否没有关心的事件)
  bool isNoneEvent() const { return events_ == kNoneEvent; }
  //关心读事件
  void enableReading() { events_ |= kReadEvent; update(); }
  //移除读事件关心
  void disableReading() { events_ &= ~kReadEvent; update(); }
  //关心写事件
  void enableWriting() { events_ |= kWriteEvent; update(); }
  //移除写事件关心
  void disableWriting() { events_ &= ~kWriteEvent; update(); }
  //移除所有关心的事件
  void disableAll() { events_ = kNoneEvent; update(); }
  //是否正在关心写事件
  bool isWriting() const { return events_ & kWriteEvent; }
  //是否正在关心读事件
  bool isReading() const { return events_ & kReadEvent; }

        执行任务队列中的任务会死锁?

        EventLoop::doPendingFunctors()是用来执行任务队列中任务的成员函数,它是私有的。

  std::vector<Functor> functors;
  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }
  for (const Functor& functor : functors)
  {
    functor();
  }

               这里,muduo库的做法是把任务队列中任务交换到了栈上一段临时空间,才执行任务队列中的任务。如果不这么做就会由死锁问题,这个需要结合把任务抛入任务队列的函数——EventLoop::queueInLoop(Functor cb)才好说明,后文再做解释~

                这里我们总结一下EventLoop::loop()。很明显,这个函数就是进行事件循环的!三步走:

        1.捞取就绪事件        2.执行就绪事件的回调        3.执行任务队列中的任务

        IO线程大多数的时候是阻塞在第一步上的,只要有事件就绪了,或者任务队列中由任务了就会被唤醒。

EventLoop保证线程安全的接口

        判断线程ID

        EventLoop提供了连个判断当前线程是否是EventLoop绑定的线程的接口:

EventLoop::assertInLoopThread() :如果不是EventLoop绑定的线程直接退出程序,用于一些强硬的检查,如果EventLoop::assertInLoopThread()失败,说明程序有逻辑有漏洞和线程安全问题,其生产的数据也就不一定可靠。

EventLoop::isInLoopThread() : 只是判断当前线程是否是EventLoop绑定的线程,如果是返回true,不是就返回flase

        把任务抛入到任务队列,死锁?

        还有一个接口是把任务放到任务队列中,EventLoop::queueInLoop,这里我要说明为什么EventLoop::doPendingFunctors()会有死锁问题。首先来看一下EventLoop::queueInLoop的实现。

  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(std::move(cb));  
  }
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }

        细心的读者可能会发现,EventLoop::queueInLoop和EventLoop::doPendingFunctors用的是同一个互斥量。如果EventLoop::doPendingFunctors在执行任务时,执行了EventLoop::queueInLoop任务,那么这个IO线程是永远也锁不住EventLoop::queueInLoop中的mutex_,因为当前的IO线程已经在EventLoop::doPendingFunctors的时候锁住互斥量,此时就是自己已经申请到了一把锁,但又去申请了,不出意外,当前线程肯定要被阻塞在EventLoop::queueInLoop中的mutex_上了。

        我们再来回顾一下前文,很明显muduo库避免了这种事情,因为muduo库把任务队列中的任务交换到了临时空间,在执行任务时就归还了锁。

        唤醒IO线程的条件?

        观察EventLoop::queueInLoop的实现会发现——不是只要有人向任务队列中抛入任务都需要用wakeup()唤醒,他是有判断条件的。思考为什么是这个条件?

        只要捋一下事件循环的流程就很容易想明白。

        1. 不是EventLoop绑定的线程在向任务抛任务,那么被绑定的IO线程大概率阻塞在事件循环中,需要被唤醒

        2.如果是EventLoop绑定的线程,但正在执行任务队列中的任务,但可能没有执行到刚刚抛入的任务,就要执行新一轮的事件循环,可能会被再次阻塞,所以这种情况需要唤醒线程,保证IO线程一定不会阻塞在就绪队列上

        执行任务队列的接口

                EventLoop::runInLoop,它的逻辑很简单,判断当前线程是否是EventLoop绑定的线程,如果是直接执行,如果不是就抛入到任务队列中去。

void EventLoop::runInLoop(Functor cb)
{
  if (isInLoopThread())
  {
    cb();
  }
  else
  {
    queueInLoop(std::move(cb));
  }
}

EventLoop终止事件循环

        EventLoop::quit()用于终止事件循环。我们来看一下它的实现。

void EventLoop::quit()
{
  quit_ = true;
  if (!isInLoopThread())
  {
    wakeup();
  }
}

        也很简单,只是把quit_改为了true。但此时IO可能阻塞在epoll模型上的就绪队列上,需要唤醒IO线程,让IO线程走完当前循环,在下一轮循环不满足条件后,结束事件循环,所以事件循环的结束不是立即生效的。

        思考,为什么IO线程调用quit的时候不用wakeup()?

       IO线程在执行EventLoop::quit(),说明IO线程已经苏醒,正在执行事件的回调或任务队列中的任务,说当前IO线程能不被阻塞的进入下一轮事件循环。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值