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";
}
}