目录
引言
本文适合于了解多路转接,了解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类为例,他其实就是封装了下面三个系统调用
所以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线程能不被阻塞的进入下一轮事件循环。