【Muduo源码分析】muduo的EventLoop 解读

Muduo奉行的是每个one loop per thread,意思是每个线程只有一个EventLoop对象。在Muduo中,称创建了EventLoop对象的线程是IO线程。

我主要关注大体框架,有些细节暂时不关注。首先看看构造函数(下列源码源自Muduo的教程示例)

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    callingPendingFunctors_(false),
    threadId_(CurrentThread::tid()),
    poller_(new EPoller(this)),
    timerQueue_(new TimerQueue(this)),
    wakeupFd_(createEventfd()),
    wakeupChannel_(new Channel(this, wakeupFd_))
{
  LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
  if (t_loopInThisThread)
  {
    LOG_FATAL << "Another EventLoop " << t_loopInThisThread
              << " exists in this thread " << threadId_;
  }
  else
  {
    t_loopInThisThread = this;
  }
  wakeupChannel_->setReadCallback(
      boost::bind(&EventLoop::handleRead, this));
  // we are always reading the wakeupfd
  wakeupChannel_->enableReading();
}

第一个问题,Muduo如何保证每个线程只能创建一个EventLoop对象? 

原因是Eventloop使用了TLS(线程局部存储)

__thread EventLoop* t_loopInThisThread = 0;

EventLoop对象在构造时,会去检查本线程是已经创建了其他的EventLoop对象。如果没有创建EventLoop对象,那么t_loopInThisThread将是一个空指针,然后将本EventLoop对象的指针赋给t_loopInThisThread。如果本线程再次创建EventLoop对象时,去检测t_loopInThisThread,发现不为空,说明已经创建了其他对象,就进行终止日志输出。

由于是TLS,所以每个线程只会关注每个线程的创建情况,如果是全局变量,那么多个线程之间无法区分创建情况(个人理解,希望指正)

第二个问题,如何判断一个线程是否是IO线程?

比对当前线程的线程ID是否是EventLoop对象的创建线程ID。对象在创建时,保存了线程ID

bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }

第三个问题,loop成员函数做什么?

首先看代码,代码如下。

void EventLoop::loop()
{
  assert(!looping_);
  assertInLoopThread();
  looping_ = true;
  quit_ = false;

  while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      (*it)->handleEvent(pollReturnTime_);
    }
    doPendingFunctors();
  }

  LOG_TRACE << "EventLoop " << this << " stop looping";
  looping_ = false;
}

loop之前的一些常规检查就不必多说。EventLoop对象有一个数据成员activeChannels_。定义如下,用来维护活跃的Channel对象,活跃是指可读、可写、或者出错。篇幅有限,Channel的分析就不在这篇博客中分享了。

typedef std::vector ChannelList;
ChannelList activeChannels_;

loop每一次迭代时,首先清空之前的activeChannels_。然后使用poller_调用poll,获取最新的活跃Channel,添加至activeChannels_。然后遍历activeChannels_,执行channel对象的handleEvent成员函数。最后执行doPendingFunctors,其中保存着希望在IO线程中执行的函数。

poller就是IO复用函数的封装,可以是select,poll,epoll。所以loop函数大部分时间会阻塞在poll函数上。这也符合非阻塞IO模型的逻辑。

第四个问题,如何将用户的函数放在IO线程中执行?

使用runInLoop成员函数

void EventLoop::runInLoop(const Functor& cb)
{
  if (isInLoopThread())
  {
    cb();
  }
  else
  {
    queueInLoop(cb);
  }
}

首先判断当前线程是否是IO线程,如果当前线程就是IO线程,那么就直接执行需要执行的函数。如果是其他线程,那么就将需要执行的函数放在队列中,等待执行,使用queueInLoop函数。

void EventLoop::queueInLoop(const Functor& cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);
  }

  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

由于可能出现多个线程同时调用queueInloop,所以向队列中添加时,需要加锁,保证线程安全。然后唤醒loop,因为上面说了,loop大部分时间会阻塞在poll上,直到有描述符发生事件。但是我此时有线程希望将函数放在io线程中执行,那么怎么办呢?这里Muduo使用了大多数网络库使用的技巧,向一个描述符中写一个字节(这个描述符在EventLoop对象构造时已经被添加关注),以便程序中poll中返回。返回后就可以执行doPendingFunctors函数了。这个函数就是处理希望在IO线程中执行的函数,使用了一个队列保存回调函数。实现如下

void EventLoop::doPendingFunctors()
{
  std::vector functors;
  callingPendingFunctors_ = true;

  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }

  for (size_t i = 0; i < functors.size(); ++i)
  {
    functors[i]();
  }
  callingPendingFunctors_ = false;
}

为了减少对临界的占用,先将队列中的值保存出来,然后执行。

整体流程图如下

【Muduo源码分析】muduo的EventLoop 解读_第1张图片

这就是EventLoop大致执行的流程,能够对Muduo有一定了解,当然其中也包含一些的细节部分去考究。

你可能感兴趣的:(Muduo,Muduo,Linux网络编程)