muduo笔记 网络库(五)事件循环EventLoop
事件驱动与EventLoop
前面(muduo笔记 网络库(一)总览)讲过,muduo网络库处理事件是Reactor模式,one loop per thread,一个线程一个事件循环。这个循环称为EventLoop,这种以事件为驱动的编程模式,称为事件驱动模式。
这种事件驱动模型要求所有任务是非阻塞的,其典型特点是:
如果一个任务需要很长时间才能完成,或者中间可能导致阻塞,就需要对任务进行分段,将其设置为非阻塞的,每次监听到前次任务完成,触发事件回调,从而接着完成后续任务。例如,要发送一个大文件,可以先发送一段,完成后,在写完成事件回调中又发送下一段,这样每次都发生一段,从而完成整个文件发送。
EventLoop是实现事件驱动模型的关键之一。核心是为线程提供运行循环,不断监听事件、处理事件,为用户提供在loop循环中运行的接口。
还是那张图,EventLoop实现事件驱动相关类图关系:
聚合关系:has-a,表示拥有的关系,两种生命周期没有必然关联,可以独立存在。
组合关系:contain-a,表包含的关系,是一种强聚合关系,强调整体与部分,生命周期一致。
EventLoop
EventLoop是一个接口类,不宜暴露太多内部细节给客户,接口及其使用应尽量简洁。EventLoop的主要职责是:
1)提供定时执行用户指定任务的方法,支持一次性、周期执行用户任务;
2)提供一个运行循环,每当Poller监听到有通道对应事件发生时,会将通道加入激活通道列表,运行循环要不断从取出激活通道,然后调用事件回调处理事件;
3)每个EventLoop对应一个线程,不允许一对多或者多对一,提供判断当前线程是否为创建EventLoop对象的线程的方法;
4)允许在其他线程中调用EventLoop的public接口,但同时要确保线程安全;
下面来看看EventLoop类声明:
/**
* Reactor模式, 每个线程最多一个EventLoop (One loop per thread).
* 接口类, 不要暴露太多细节给客户
*/
class EventLoop : public noncopyable
{
public:
typedef std::function<void()> Functor;
EventLoop();
~EventLoop(); // force out-line dtor, for std::unique_ptr members.
/* loop循环, 运行一个死循环.
* 必须在当前对象的创建线程中运行.
*/
void loop();
/*
* 退出loop循环.
* 如果通过原始指针(raw pointer)调用, 不是100%线程安全;
* 为了100%安全, 最好通过shared_ptr<EventLoop>调用
*/
void quit();
/*
* Poller::poll()返回的时间, 通常意味着有数据达到.
* 对于PollPoller, 是调用完poll(); 对于EPollPoller, 是调用完epoll_wait()
*/
Timestamp pollReturnTime() const { return pollReturnTime_; }
/* 获取loop循环次数 */
int64_t iterator() const { return iteration_; }
/*
* 在loop线程中, 立即运行回调cb.
* 如果没在loop线程, 就会唤醒loop, (排队)运行回调cb.
* 如果用户在同一个loop线程, cb会在该函数内运行; 否则, 会在loop线程中排队运行.
* 因此, 在其他线程中调用该函数是安全的.
*/
void runInLoop(Functor cb);
/* 排队回调cb进loop线程.
* 回调cb在loop中完成polling后运行.
* 从其他线程调用是安全的.
*/
void queueInLoop(Functor cb);
/* 排队的回调cb个数 */
size_t queueSize() const;
// timers
/*
* 在指定时间点运行回调cb.
* 从其他线程调用安全.
*/
TimerId runAt(Timestamp time, TimerCallback cb);
/*
* 在当前时间点+delay延时后运行回调cb.
* 从其他线程调用安全.
*/
TimerId runAfter(double delay, TimerCallback cb);
/*
* 每隔interval sec周期运行回调cb.
* 从其他线程调用安全.
*/
TimerId runEvery(double interval, TimerCallback cb);
/*
* 取消定时器, timerId唯一标识定时器Timer
* 从其他线程调用安全.
*/
void cancel(TimerId timerId);
// internal usage
/* 唤醒loop线程, 没有事件就绪时, loop线程可能阻塞在poll()/epoll_wait() */
void wakeup();
/* 更新Poller监听的channel, 只能在channel所属loop线程中调用 */
void updateChannel(Channel* channel);
/* 移除Poller监听的channel, 只能在channel所属loop线程中调用 */
void removeChannel(Channel* channel);
/* 判断Poller是否正在监听channel, 只能在channel所属loop线程中调用 */
bool hasChannel(Channel* channel);
// pid_t threadId() const { return threadId_; }
/* 断言当前线程是创建当前对象的线程, 如果不是就终止程序(LOG_FATAL) */
void assertInLoopThread();
/* 判断前线程是否创建当前对象的线程.
* threadId_是创建当前EventLoop对象时, 记录的线程tid
*/
bool isInLoopThread() const;
/*
* 判断是否有待调用的回调函数(pending functor).
* 由其他线程调用runAt/runAfter/runEvery, 会导致回调入队列待调用.
*/
bool callingPendingFunctors() const
{ return callingPendingFunctors_; }
/*
* 判断loop线程是否正在处理事件, 执行事件回调.
* loop线程正在遍历,执行激活channels时, eventHandling_会置位; 其余时候, 会清除.
*/
bool eventHandling() const
{ return eventHandling_; }
/* context_ 用于应用程序传参, 由网络库用户定义数据 */
void setContext(const boost::any& context)
{ context_ = context; }
const boost::any& getContext() const
{ return context_; }
boost::any* getMutableContext()
{ return &context_; }
/* 获取当前线程的EventLoop对象指针 */
static EventLoop* getEventLoopOfCurrentThread();
private:
/* 终止程序(LOG_FATAL), 当前线程不是创建当前EventLoop对象的线程时,
* 由assertInLoopThread()调用 */
void abortNotInLoopThread();
/* 唤醒所属loop线程, 也是wakeupFd_的事件回调 */
void handleRead(); // waked up
/* 处理pending函数 */
void doPendingFunctors();
/* 打印激活通道的事件信息, 用于debug */
void printActiveChannels() const; // DEBUG
typedef std::vector<Channel*> ChannelList;
bool looping_; /* atomic, true表示loop循环执行中 */
std::atomic<bool> quit_; /* loop循环退出条件 */
bool eventHandling_; /* atomic, true表示loop循环正在处理事件回调 */
bool callingPendingFunctors_; /* atomic, true表示loop循环正在调用pending函数 */
int64_t iteration_; /* loop迭代次数 */
const pid_t threadId_; /* 线程id, 对象构造时初始化 */
Timestamp pollReturnTime_; /* poll()返回时间点 */
std::unique_ptr<Poller> poller_; /* 轮询器, 用于监听事件 */
std::unique_ptr<TimerQueue> timerQueue_; /* 定时器队列 */
int wakeupFd_; /* 唤醒loop线程的eventfd */
/* 用于唤醒loop线程的channel.
* 不像TimerQueue是内部类, 不应该暴露Channel给客户. */
std::unique_ptr<Channel> wakeupChannel_;
boost::any context_; /* 用于应用程序通过当前对象传参的变量, 由用户定义数据 */
/* 临时辅助变量 */
ChannelList activeChannels_; /* 激活事件的通道列表 */
Channel* currentActiveChannel_; /* 当前激活的通道, 即正在调用事件回调的通道 */
mutable MutexLock mutex_;
/* 待调用函数列表, 存放不在loop线程的其他线程调用 runAt/runAfter/runEvery, 而要运行的函数 */
std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
};
EventLoop不可拷贝,因为与之关联的不仅对象本身,还有线程以及thread local数据等资源。
在这里,可以把EventLoop功能简要分为这几大部分:
1)提供运行循环;
2)运行定时任务,一次性 or 周期;
3)处理激活通道事件;
4)线程安全;
对于1),loop()提供运行循环,quit()退出循环,iterator()查询循环次数,wakeup()用于唤醒loop线程,handleRead()读取唤醒消息。
对于2),runInLoop()在loop线程中“立即”运行一次用户任务,runAt()/runAfter()添加一次性定时任务,runEvery()添加周期定时任务,doPendingFunctors()回调所有的pending函数,vector pendingFunctors_用于排队待处理函数到loop线程执行,queueSize()获取该vector大小;cancel()取消定时任务。
对于3),updateChannel()/removeChannel()/hasChannel()用于通道更新/移除/判断,vector activeChannels_存储当前所有激活的通道,currentActiveChannel_存储当前正在处理的激活通道;
对于4),isInLoopThread()/assertInLoopThread()判断/断言 当前线程是创建当前EventLoop对象的线程,互斥锁mutex_用来做互斥访问需要保护数据。
值得一提的是,boost::any类型的成员context_用来给用户提供利用EventLoop传数据的方式,相当于C里面的void*,用户可利用boost::any_cast进行转型。
EventLoop的构造与析构
EventLoop代表一个事件循环,是one loop per thread的体现,每个线程只能有一个EventLoop对象。
EventLoop构造函数要点:
1)检查当前线程是否已经创建了EventLoop对象,遇到错误就终止程序(LOG_FATAL);
2)记住本对象所属线程id(threadId_);
析构函数要点:
1)清除当前线程EventLoop指针,便于下次再创建EventLoop对象。
__thread EventLoop* t_loopInThisThread = 0; // thread local变量, 指向当前线程创建的EventLoop对象
EventLoop::EventLoop()
: looping_(false),
threadId_(CurrentThread::tid()),
{
LOG_DEBUG << "EventLoop create " << this << " in thread " << threadId_;
if (t_loopInThisThread) // 当前线程已经包含了EventLoop对象
{
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
}
else // 当前线程尚未包含EventLoop对象
{
t_loopInThisThread = this;
}
}
EventLoop::~EventLoop()
{
LOG_DEBUG << "EventLoop " << this << " of thread " << threadId_
<< " destructs in thread " << CurrentThread::tid();
t_loopInThisThread = NULL;
}
可以通过thread local变量t_loopInThisThread指向创建的EventLoop对象,来确保每个线程只有一个EventLoop对象。同一个线程内,可通过static函数getEventLoopOfCurrentThread,返回该EventLoop对象指针。
EventLoop *EventLoop::getEventLoopOfCurrentThread() // static
{
return t_loopInThisThread;
}
特定线程检查,确保线程安全
有些成员函数只能在EventLoop对象所在线程调用,如何检查该前提条件(pre-condition)?
EventLoop提供了isInLoopThread()、assertInLoopThread(),分别用于判断、断言 当前线程为创建EventLoop对象线程。
从下面实现可看到,assertInLoopThread()断言失败时,调用abortNotInLoopThread()终止程序(LOG_FATAL)。
void EventLoop::assertInLoopThread() // 断言当前线程(tid())是调用当前EventLoop对象的持有者线程(threadId_)
{
if (!isInLoopThread())
{
abortNotInLoopThread(); // 断言失败则终止程序
}
}
bool EventLoop::isInLoopThread() const // 判断当前线程是否为当前EventLoop对象的持有者线程
{ return threadId_ == CurrentThread::tid(); }
void EventLoop::abortNotInLoopThread() // LOG_FATAL 终止程序
{
LOG_FATAL << "EventLoop::abortNotInLoopThread - EventLoop " << this
<< " was created in threadId_ = " << threadId_
<< ", current thread id = " << CurrentThread::tid();
}
loop循环
提供运行循环,不断监听事件、处理事件。
/**
* 真正的工作循环.
* 获得所有当前激活事件的通道,用Poller->poll()填到activeChannels_,
* 然后调用Channel::handleEvent()处理每个激活通道.
*
* 最后排队运行所有pending函数, 通常是其他线程通过loop来调用运行用户任务
*/
void EventLoop::loop()
{
assert(!looping_); // to avoid reduplicate loop
assertInLoopThread(); // to avoid new EventLoop() and loop() are not one thread
looping_ = true;
quit_ = false; // FIXME: what if someone calls quit() before loop() ?
LOG_TRACE << "EventLoop " << this << " start looping";
while (!quit_)
{
activeChannels_.clear(); // 清除激活事件的通道列表
// 监听所有通道, 可能阻塞线程, 所有激活事件对应通道会填入activeChannels_
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
++iteration_; // 循环次数+1
if (Logger::logLevel() <= Logger::TRACE)
{
printActiveChannels();
}
// TODO sort channel by priority
// 处理所有激活事件
eventHandling_ = true;
for (Channel* channel : activeChannels_)
{
currentActiveChannel_ = channel;
// 通过Channel::handleEvent回调事件处理函数
currentActiveChannel_->handleEvent(pollReturnTime_);
}
currentActiveChannel_ = NULL;
eventHandling_ = false;
// 运行pending函数, 由其他线程请求调用的用户任务
doPendingFunctors();
}
LOG_TRACE << "EventLoop " << this << " stop looping";
looping_ = false;
}
loop线程运行事件回调的关键是,用Poller::poll()将激活事件的通道填入通道列表activeChannels_,然后逐一调用每个通道的handleEvent,从而调用为Channel注册的事件回调来处理事件。
添加、更新、删除通道
loop循环用来处理激活事件,那用户如何更新添加、更新、删除通道事件呢?
前面已经提到,可以用updateChannel/removeChannel 更新/移除 Poller 监听的通道。
关于 Poller这部分,可参见 muduo笔记 网络库(二)I/O复用封装Poller
/**
* 根据具体poller对象, 来更新通道.
* 会修改poller对象监听的通道数组.
* @note 必须在channel所属loop线程运行
*/
void EventLoop::updateChannel(Channel *channel)
{
assert(channel->ownerLoop() == this);
assertInLoopThread();
poller_->updateChannel(channel);
}
/**
* 根据具体poller对象, 来删除通道.
* 会删除poller对象监听的通道数组.
* @note 如果待移除通道正在激活事件队列, 应该先从激活事件队列中移除
*/
void EventLoop::removeChannel(Channel *channel)
{
assert(channel->ownerLoop() == this);
assertInLoopThread();
if (eventHandling_)
{
assert(currentActiveChannel_ == channel ||
std::find(activeChannels_.begin(), activeChannels_.end(), channel) == activeChannels_.end());
}
poller_->removeChannel(channel);
}
另外,可用hasChannel来判断Poller是否正在监听channel。
/**
* 判断poller是否正在监听通道channel
* @note 必须在channel所属loop线程运行
*/
bool EventLoop::hasChannel(Channel *channel)
{
assert(channel->ownerLoop() == this);
assertInLoopThread();
return poller_->hasChannel(channel);
}
定时任务
用户调用runAt/runAfter/runEvery 运行定时任务,调用cancel取消定时任务。
/**
* 定时功能,由用户指定绝对时间
* @details 每为定时器队列timerQueue添加一个Timer,
* timerQueue内部就会新建一个Timer对象, TimerId就保含了这个对象的唯一标识(序列号)
* @param time 时间戳对象, 单位1us
* @param cb 超时回调函数. 当前时间超过time代表时间时, EventLoop就会调用cb
* @return 一个绑定timerQueue内部新增的Timer对象的TimerId对象, 用来唯一标识该Timer对象
*/
TimerId EventLoop::runAt(Timestamp time, TimerCallback cb)
{
return timerQueue_->addTimer(std::move(cb), time, 0.0);
}
/**
* 定时功能, 由用户相对时间, 通过runAt实现
* @param delay 相对时间, 单位s, 精度1us(小数)
* @param cb 超时回调
*/
TimerId EventLoop::runAfter(double delay, TimerCallback cb)
{
Timestamp time(addTime(Timestamp::now(), delay));
return runAt(time, std::move(cb));
}
/**
* 定时功能, 由用户指定周期, 重复运行
* @param interval 运行周期, 单位s, 精度1us(小数)
* @param cb 超时回调
* @return 一个绑定timerQueue内部新增的Timer对象的TimerId对象, 用来唯一标识该Timer对象
*/
TimerId EventLoop::runEvery(double interval, TimerCallback cb)
{
Timestamp time(addTime(Timestamp::now(), interval));
return timerQueue_->addTimer(std::move(cb), time, interval);
}
/**
* 取消指定定时器
* @param timerId Timer id, 唯一对应一个Timer对象
*/
void EventLoop::cancel(TimerId timerId)
{
return timerQueue_->cancel(timerId);
}
用户运行一个loop线程,并添加定时任务示例:
void threadFunc()
{
assert(EventLoop::getEventLoopOfCurrentThread() == NULL); // 断言当前线程没有创建EventLoop对象
EventLoop loop; // 创建EventLoop对象
assert(EventLoop::getEventLoopOfCurrentThread() == &loop); // 断言当前线程创建了EventLoop对象
loop.runAfter(1.0, callback); // 1sec后运行callback
loop.loop(); // 启动loop循环
}
runInLoop与queueInLoop执行用户任务
同样是运行用户任务函数,runInLoop和queueInLoop都可以被多个线程执行,分为2种情况:1)如果当前线程是创建当前EventLoop对象的线程,那么立即执行用户任务;2)如果不是,那么在loop循环中排队执行(本次循环末尾),实际上这点也是由queueInLoop完成的。
queueInLoop只做了runInLoop的第2)种情况的工作,也就是只会在loop循环中排队执行用户任务。
为什么要对pendingFunctors_加锁?
因为queueInLoop可以被多个线程访问,意味着pendingFunctors_也能被多个线程访问,加锁确保线程安全。
/**
* 执行用户任务
* @param cb 用户任务函数
* @note 可以被多个线程执行:
* 如果当前线程是创建当前EventLoop对象的线程,直接执行;
* 否则,用户任务函数入队列pendingFunctors_成为一个pending functor,在loop循环中排队执行
*/
void EventLoop::runInLoop(Functor cb)
{
if (isInLoopThread())
{
cb();
}
else
{
queueInLoop(std::move(cb));
}
}
/**
* 排队进入pendingFunctors_,等待执行
* @param cb 用户任务函数
* @note 如果当前线程不是创建当前EventLoop对象的线程 或者正在调用pending functor,
* 就唤醒loop线程,避免loop线程阻塞.
*/
void EventLoop::queueInLoop(Functor cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(std::move(cb));
}
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}
eventfd与wakeup()唤醒
有2处可能导致loop线程阻塞:
1)Poller::poll()中调用poll(2)/epoll_wait(7) 监听fd,没有事件就绪时;
2)用户任务函数调用了可能导致阻塞的函数;
而当EventLoop加入用户任务时,loop循环是没办法直接知道的,要避免无谓的等待,就需要及时唤醒loop线程。
muduo用eventfd技术,来唤醒线程。
eventfd原理
eventfd是Linux特有的(Linux 2.6以后),专用于事件通知的机制,类似于管道(pipe)、域套接字(UNIX Domain Socket)。
创建eventfd 函数原型:
#include <sys/eventfd.h>
/* 创建一个文件描述符(event fd), 用于事件通知
* initval 计数初值
* flags 标志位, 如果没用到可设为0, 也可以用以下选项 按位或 取值:
* EFD_CLOEXEC 为新建的fd设置close-on-exec(FD_CLOEXEC), 等效于以O_CLOEXEC方式open(2)
* EFD_NONBLOCK 等效于fcntl(2)设置O_NONBLOCK
* EFD_SEMAPHORE 将eventfd当信号量一样调用, read 将导致计数-1, write 将导致计数+1; 如果没指定该标志, read将返回8byte计数值, 且计数值归0, write将计数值+指定值.
* 返回 新建的fd, 用于事件通知, 绑定到一个eventfd对象; 失败, 返回-1
*/
int eventfd(unsigned int initval, int flags);
创建完event fd后,可用read(2)读取event fd,如果fd是阻塞的,read可能阻塞线程;如果event fd设置了EFD_NONBLOCK,read返回EAGIAN错误。直到另外一个线程对event fd进行write。
// 为wakeupChannel_设置读回调
wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead, this));
// we are always reading the wakeupfd
// 使能wakeupChannel_读事件
wakeupChannel_->enableReading();
eventfd使用示例:
线程1阻塞等待,线程2唤醒线程1。
#include <stdio.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <pthread.h>
#define __STDC_FORMAT_MACROS // for 跨平台打印
#include <inttypes.h>
void* thread_func1(void* arg) /* 等待线程 */
{
int wakeupfd = *(int*)arg;
printf("thread_func1 start\n");
uint64_t rdata;
int ret = read(wakeupfd, &rdata, sizeof(rdata));
if (ret < 0) {
perror("thread_func1 read error");
pthread_exit(NULL);
}
printf("thread_func1 success to be waked up, rdata = %" PRId64 "\n", rdata);
}
void* thread_func2(void* arg) /* 唤醒线程 */
{
int wakeupfd = *(int*)arg;
printf("thread_func2 ready to sleep 1 sec\n");
sleep(1);
uint64_t wdata = 10;
int ret = write(wakeupfd, &wdata, sizeof(wdata));
if (ret < 0) {
perror("thread_func2 write error");
pthread_exit(NULL);
}
printf("thread_func2 success to wake up another thread, wdata = %" PRId64 "\n", wdata);
}
/* 创建2个线程,thread_func1阻塞等待eventfd,thread_func2唤醒等等eventfd的线程 */
int main()
{
int evfd = eventfd(0, 0);
if (evfd < 0) {
perror("eventfd error");
exit(1);
}
pthread_t th1, th2;
pthread_create(&th1, NULL, thread_func1, (void*)&evfd);
pthread_create(&th2, NULL, thread_func2, (void*)&evfd);
pthread_join(th1, NULL);
pthread_join(th2, NULL);
return 0;
}
EventLoop使用eventfd唤醒loop线程
1)创建event fd
构造函数中,wakeupFd_ 初值为createEventfd()
int createEventfd()
{
int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (evtfd < 0)
{
LOG_SYSERR << "Failed in eventfd";
abort();
}
return evtfd;
}
2)绑定event fd与唤醒通道wakeupChannel_利用event fd构造一个Channel对象后,传递给wakeupChannel_,便于Poller监听、事件回调
// 为wakeupChannel_设置读回调
wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead, this));
// we are always reading the wakeupfd
// 使能wakeupChannel_读事件
wakeupChannel_->enableReading();
3)启动loop循环,可能阻塞在poll(2)/epoll_wait(7)
4)其他线程通过queueInLoop()调用wakeup(),唤醒阻塞的loop线程
/**
* 其他线程唤醒等待在wakeupFd_上的线程, 产生读就绪事件.
* @note write将添加8byte数据到内部计数器. 被唤醒线程必须调用read读取8byte数据.
*/
void EventLoop::wakeup()
{
uint64_t one = 1;
ssize_t n = sockets::write(wakeupFd_, &one, sizeof(one));
if (n != sizeof(one))
{
LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
}
}
5)loop线程被唤醒后,读取event fd
/**
* 处理wakeupChannel_读事件
* @note read wakeupfd_
*/
void EventLoop::handleRead()
{
uint64_t one = 1;
ssize_t n = sockets::read(wakeupFd_, &one, sizeof(one));
if (n != sizeof(one))
{
LOG_ERROR << "EventLoop::handleRead() reads " << n << " bytes instead of 8";
}
}
参考
muduo网络库学习(四)事件驱动循环EventLoop | CSDN
muduo库其它部分解析参见:muduo库笔记汇总