muduo网络库学习笔记(11):有用的runInLoop()函数

runInLoop()函数的有用之处

“EventLoop有一个非常有用的功能:在它的IO线程内执行某个用户任务回调,即EventLoop::runInLoop(const Functor& cb),其中Functor是boost::function<void()>。如果用户在当前IO线程调用这个函数,回调会同步进行;如果用户在其他线程调用runInLoop(),cb会被加入队列,IO线程会被唤醒来调用这个Functor。”

即我们可以在线程间方便地进行任务调配,而且可以在不用锁的情况下保证线程安全。

下面通过对代码的分析来一探究竟。

源码分析

(1)开门见山,我们先来看runInLoop()函数
流程图:
这里写图片描述

代码片段1:EventLoop::runInLoop()
文件名:EventLoop.cc

// 在IO线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
  if (isInLoopThread())
  {
    // 如果是当前IO线程调用runInLoop,则同步调用cb
    cb();
  }
  else
  {
    // 如果是其它线程调用runInLoop,则异步地将cb添加到队列
    queueInLoop(cb);
  }
}

函数的逻辑很简单:判断是否处于当前IO线程,是则执行这个函数,如果不是则将函数加入队列。

(2)queueInLoop()函数
流程图:
这里写图片描述

代码片段2:EventLoop::queueInLoop()
文件名:EventLoop.cc

void EventLoop::queueInLoop(const Functor& cb)
{
  // 把任务加入到队列可能同时被多个线程调用,需要加锁
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);
  }

  // 将cb放入队列后,我们还需要在必要的时候唤醒IO线程来处理
  // 必要的时候有两种情况:
  // 1.如果调用queueInLoop()的不是IO线程,需要唤醒
  // 2.如果在IO线程调用queueInLoop(),且此时正在调用pending functor,需要唤醒
  // 即只有在IO线程的事件回调中调用queueInLoop()才无需唤醒
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

唤醒的时间点是怎么选择的呢?我们来回顾一下事件循环EventLoop::loop()中的一段代码:

代码片段3:EventLoop::loop()部分
文件名:EventLoop.cc

while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    // 执行pending Functors_中的任务回调
    // 这种设计使得IO线程也能执行一些计算任务,避免了IO线程在不忙时长期阻塞在IO multiplexing调用中
    doPendingFunctors();
  }

这里写图片描述
I.第一种情况易理解:调用queueInLoop的线程不是当前IO线程时,则需要唤醒当前IO线程,才能及时执行doPendingFunctors()。

II.第二种情况,调用queueInLoop()的线程是当前IO线程,比如在doPendingFunctors()中执行functors[i]() 时又调用了queueInLoop()。此时doPendingFunctors() 执行functors[i]() 过程中又添加了任务,故循环回去到poll的时候需要被唤醒返回,进而继续执行doPendingFunctors() 。

只有在当前IO线程的事件回调中调用queueInLoop才不需要唤醒,即在handleEvent()中调用queueInLoop ()不需要唤醒,因为接下来马上就会执行doPendingFunctors()。

(3)doPendingFunctors()函数
EventLoop::doPendingFunctors()不是简单地在临界区依次调用Functor,而是把回调列表swap()到局部变量functors中,这样做,一方面减小了临界区的长度(不会阻塞其他线程调用queueInLoop()),另一方面避免了死锁(因为Functor可能再调用queueInLoop())。

代码片段4:EventLoop::doPendingFunctors()
文件名:EventLoop.cc

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

  // 把回调列表swap()到局部变量functors中
  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }

  // 依次执行回调列表中的函数
  for (size_t i = 0; i < functors.size(); ++i)
  {
    functors[i]();
  }
  callingPendingFunctors_ = false;
}

muduo这里没有反复执行doPendingFunctors()直到pendingFunctors_为空,反复执行可能会使IO线程陷入死循环,无法处理IO事件。

(4)我们回头再来看一下–怎样实现唤醒
传统的进程/线程间唤醒办法是用pipe或者socketpair,IO线程始终监视管道上的可读事件,在需要唤醒的时候,其他线程向管道中写一个字节,这样IO线程就从IO multiplexing阻塞调用中返回。pipe和socketpair都需要一对文件描述符,且pipe只能单向通信,socketpair可以双向通信。

下面介绍一下muduo所采用的一种高效的进程/线程间事件通知机制–eventfd。

// 头文件
#include <sys/eventfd.h> 

// 为事件通知创建文件描述符
// 参数initval表示初始化计数器值
// 参数flags可取EFD_NONBLOCK、EFD_CLOEXEC、EFD_SEMAPHORE 
int eventfd(unsigned int initval, int flags);

它的高效体现在:一方面它比 pipe 少用一个 fd,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,全部buffer只有定长8 bytes,不像 pipe 那样可能有不定长的真正 buffer。

代码片段5:EventLoop::wakeup()
文件名:EventLoop.cc

void EventLoop::wakeup()
{
  uint64_t one = 1;
  // 向wakupFd_中写入8字节从而唤醒,wakeupFd_即eventfd()所创建的文件描述符
  ssize_t n = ::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值