muduo网络库代码剖析——EventLoop类
EventLoop在网络库中的作用?
主线程也有一个线程在跑EventLoop对象的loop()函数,在这个函数内关注服务端的socketfd用来接收新的客户端socketfd连接。
将这个新的socketfd连接放到各个线程中并运行线程的EventLoop对象的loop()来关注已连接socketfd的可读可写事件。
所以EventLoop在网络库中的作用基本就是来关注fd事件并执行回调函数的。
EventLoop对象的数据成员:
typedef std::vector<Channel*> ChannelList; //线程是否有进入EventLoop的loop()函数 bool looping_; /* atomic */ //线程是否要退出EventLoop的loop()函数 std::atomic<bool> quit_; //线程是否在执行激活状态channel通道的回调函数 bool eventHandling_; /* atomic */ //EventLoop线程做的额外工作 bool callingPendingFunctors_; /* atomic */ //loop函数循环的次数 int64_t iteration_; //独一无二的线程标识,能区分不同进程间的不同线程 const pid_t threadId_; //poll有事件到来返回的时间 Timestamp pollReturnTime_; //poll函数的具体调用 std::unique_ptr<Poller> poller_; //定时器功能的实现 std::unique_ptr<TimerQueue> timerQueue_; //用于唤醒阻塞在poll函数的线程自己 int wakeupFd_; //这个channel对象是用wakeupFd_和唤醒后需要执行什么函数构造的 std::unique_ptr<Channel> wakeupChannel_; //??还未知作用 boost::any context_; // 返回所有有事件到来的channel通道 ChannelList activeChannels_; //当前正在执行哪个channel回调函数的channel指针 Channel* currentActiveChannel_; //互斥锁用来互斥地往pendingFunctors_添加额外任务 mutable MutexLock mutex_; //EventLoop线程除了执行有事件到来的Channel的回调函数外,也会执行这个vector内的函数(我叫它为额外的工作) std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
EventLoop有什么功能?
1.返回所有文件描述符fd有事件(可读可写错误)的通道channel,并执行channel通道注册的回调函数。这是EventLoop对象所在线程的主要工作。
2.EventLoop对象所处的线程会阻塞在poll函数,利用eventfd唤醒自己来做一些额外的工作(例如:添加/删除一个新定时器任务)
3.利用timerfd(TimerId类、Timer类、TimerQueue类)实现线程定时执行任务的功能。
一、EventLoop的主要工作是调用激活的Channel的回调函数
流程图:
可以看到Channel对象只是调用了handleEvent(),最终调用的函数是更上一层类或者说是Channel对象所属类注册给channel的回调函数。
二、EventLoop如何利用eventfd从poll函数中唤醒自己去做额外工作
在EventLoop的构造函数内就会设置wakeupChannel_可读事件激活时要回调的函数EventLoop::handleRead(),并将监听eventfd文件描述符事件。当可读事件到来的时候会调用
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"; } }
从eventupFd_文件描述符中取走数据,防止不断触发可读事件。
设置到wakeupChannel_可读事件是调用了:
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"; } }
流程图:
Channel类对象wakeupChannel_是如何被添加进监听列表的见:
muduo网络库代码剖析——Poller类、PollPoller类、EpollPoller类
三、利用timerfd(TimerId类、Timer类、TimerQueue类)实现线程定时执行任务的功能。
既然EventLoop对象的主要任务是执行有事件的Channel的回调函数,那么就可以利用timerfd实现定时调用回调函数的功能;
muduo网络库代码剖析——TimerId类、Timer类、TimerQueue类
EventLoop.cc文件:
1 #include "muduo/net/EventLoop.h" 2 3 #include "muduo/base/Logging.h" 4 #include "muduo/base/Mutex.h" 5 #include "muduo/net/Channel.h" 6 #include "muduo/net/Poller.h" 7 #include "muduo/net/SocketsOps.h" 8 #include "muduo/net/TimerQueue.h" 9 10 #include <algorithm> 11 12 #include <signal.h> 13 #include <sys/eventfd.h> 14 #include <unistd.h> 15 16 using namespace muduo; 17 using namespace muduo::net; 18 19 namespace 20 { 21 __thread EventLoop* t_loopInThisThread = 0; 22 23 const int kPollTimeMs = 10000; 24 25 int createEventfd() 26 { 27 int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); 28 if (evtfd < 0) 29 { 30 LOG_SYSERR << "Failed in eventfd"; 31 abort(); 32 } 33 return evtfd; 34 } 35 36 #pragma GCC diagnostic ignored "-Wold-style-cast" 37 class IgnoreSigPipe 38 { 39 public: 40 IgnoreSigPipe() 41 { 42 ::signal(SIGPIPE, SIG_IGN); 43 // LOG_TRACE << "Ignore SIGPIPE"; 44 } 45 }; 46 #pragma GCC diagnostic error "-Wold-style-cast" 47 48 IgnoreSigPipe initObj; 49 } // namespace
知识点1:匿名命名空间
知识点2:static全局变量
全局对象会在main函数之前被先调用,所以一开始IgnoreSigPipe()构造函数就被调用了。
1 EventLoop::EventLoop() 2 : looping_(false), 3 quit_(false), 4 eventHandling_(false), 5 callingPendingFunctors_(false), 6 iteration_(0), 7 threadId_(CurrentThread::tid()), 8 poller_(Poller::newDefaultPoller(this)), 9 timerQueue_(new TimerQueue(this)), 10 wakeupFd_(createEventfd()), 11 wakeupChannel_(new Channel(this, wakeupFd_)), 12 currentActiveChannel_(NULL) 13 { 14 LOG_DEBUG << "EventLoop created " << this << " in thread " << threadId_; 15 if (t_loopInThisThread) 16 { 17 LOG_FATAL << "Another EventLoop " << t_loopInThisThread 18 << " exists in this thread " << threadId_; 19 } 20 else 21 { 22 t_loopInThisThread = this; 23 } 24 wakeupChannel_->setReadCallback( 25 std::bind(&EventLoop::handleRead, this)); 26 // we are always reading the wakeupfd 27 wakeupChannel_->enableReading(); 28 } 29 30 EventLoop::~EventLoop() 31 { 32 LOG_DEBUG << "EventLoop " << this << " of thread " << threadId_ 33 << " destructs in thread " << CurrentThread::tid(); 34 wakeupChannel_->disableAll(); 35 wakeupChannel_->remove(); 36 ::close(wakeupFd_); 37 t_loopInThisThread = NULL; 38 }
构造函数:
当eventfd有可读事件到来以后,EventLoop对象注册了它需要调用的函数:EventLoop::handleRead()
第8行:
Poller* Poller::newDefaultPoller(EventLoop* loop) { if (::getenv("MUDUO_USE_POLL")) { return new PollPoller(loop); } else { return new EPollPoller(loop); } }
析构函数:
释放每个EventLoop对象持有的eventfd文件描述符。
1 void EventLoop::loop() 2 { 3 assert(!looping_); 4 assertInLoopThread(); 5 looping_ = true; 6 quit_ = false; // FIXME: what if someone calls quit() before loop() ? 7 LOG_TRACE << "EventLoop " << this << " start looping"; 8 9 while (!quit_) 10 { 11 activeChannels_.clear(); 12 pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_); 13 ++iteration_; 14 if (Logger::logLevel() <= Logger::TRACE) 15 { 16 printActiveChannels(); 17 } 18 // TODO sort channel by priority 19 eventHandling_ = true; 20 for (Channel* channel : activeChannels_) 21 { 22 currentActiveChannel_ = channel; 23 currentActiveChannel_->handleEvent(pollReturnTime_); 24 } 25 currentActiveChannel_ = NULL; 26 eventHandling_ = false; 27 doPendingFunctors(); 28 } 29 30 LOG_TRACE << "EventLoop " << this << " stop looping"; 31 looping_ = false; 32 }
每一个线程有且只有一个EventLoop对象,且在循环跑EventLoop对象的loop()函数。这个函数可谓是顶一片天的函数。
第4行:
void assertInLoopThread() { if (!isInLoopThread()) { abortNotInLoopThread(); } } void EventLoop::abortNotInLoopThread() { LOG_FATAL << "EventLoop::abortNotInLoopThread - EventLoop " << this << " was created in threadId_ = " << threadId_ << ", current thread id = " << CurrentThread::tid(); } bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
判断当前调用函数的线程是否是该EventLoop所在的线程,此函数不能跨线程调用执行。
第12~26行:
(假设已经注册了监听fd的Channel通道)返回所有有事件(包括可读可写)的Channel。并调用这些Channel注册的回调函数。
第27行:
void EventLoop::doPendingFunctors() { std::vector<Functor> functors; callingPendingFunctors_ = true; { MutexLockGuard lock(mutex_); functors.swap(pendingFunctors_); } for (const Functor& functor : functors) { functor(); } callingPendingFunctors_ = false; }
EventLoop除了会执行Channel注册的回调函数外,还可能会执行其他的一些事件。这些事件都会放在vector<Functor> pendingFunctors_内;
这些事件包括:
1.新Timer加入到TimerQueue内。
2.从TimerQueue删除定时器任务。
其中为什么要在doPendingFunctors内加锁,且是用局部作用的方式加锁??
假设有两个线程A和B:
B线程有可能会继续在调用线程A的EventLoop::runInLoop()让往pendingFunctors_中添加任务,由于线程B往线程A的pendingFunctors加Functor, 线程A在调用pendingFunctors中的Functor,由于两个线程在操作同一个变量,那么在线程A调用pendingFunctors_的Functor时就需要加锁。
这里的加锁还很巧妙。直接将所有的Functor交换出来,一方面是减少了临界区的长度。一方面也避免了死锁:如果Functor又调用自身的EventLoop::runInLoop(),如果把调用Functor也加锁的话,就会造成死锁。
让线程退出,即让EventLoop对象所在的线程退出loop()函数:
1 void EventLoop::quit() 2 { 3 quit_ = true; 4 // There is a chance that loop() just executes while(!quit_) and exits, 5 // then EventLoop destructs, then we are accessing an invalid object. 6 // Can be fixed using mutex_ in both places. 7 if (!isInLoopThread()) 8 { 9 wakeup(); 10 } 11 } 12 13 void EventLoop::wakeup() 14 { 15 uint64_t one = 1; 16 ssize_t n = sockets::write(wakeupFd_, &one, sizeof one); 17 if (n != sizeof one) 18 { 19 LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8"; 20 } 21 }
为什么如果当前线程不是EventLoop对象所在的线程,要利用eventFd唤醒线程呢?
因为有可能想让那个线程退出但是线程阻塞在了poll/epoll上,因此唤醒它来退出。
那为什么当前线程是EventLoop对象所在的线程就不需要唤醒呢?
因为如果当前线程能执行到quit()函数,所有当前EventLoop对象所在的线程并没有阻塞,等处理完一个事件一个会回到判断while(!quit_)然后正常退出的。
四、EventLoop线程做的额外事情(除了处理fd可读可写事件外)
1 void EventLoop::runInLoop(Functor cb) 2 { 3 if (isInLoopThread()) 4 { 5 cb(); 6 } 7 else 8 { 9 queueInLoop(std::move(cb)); 10 } 11 } 12 13 void EventLoop::queueInLoop(Functor cb) 14 { 15 { 16 MutexLockGuard lock(mutex_); 17 pendingFunctors_.push_back(std::move(cb)); 18 } 19 20 if (!isInLoopThread() || callingPendingFunctors_) 21 { 22 wakeup(); 23 } 24 }
runInLoop()的字面意思是:在EventLoop对象所在的线程调用。
此时又分两种情况:
1.调用runInLoop()的线程是EventLoop对象所在的线程,那么这个线程直接调用这个事件就可以了(Functor cb)。
2.如果调用runInLoop()的线程不是EventLoop对象所在的线程,那么加锁放入vector<Functor> pendingFunctors_内,然后唤醒EventLoop对象所在的线程来处理。
不过有可能EventLoop对象所处的线程正在执行eventHandling_,那么会发生什么呢?在处理完eventHandling后,会doPendingFunctors,这个时候额外事件是已经添加进来了。那么唤醒poll的eventfd又会额外调用一次doPendingFunctors,会不会有问题呢??
五、EventLoop对定时事件的实现
TimerId EventLoop::runAt(Timestamp time, TimerCallback cb) { return timerQueue_->addTimer(std::move(cb), time, 0.0); } TimerId EventLoop::runAfter(double delay, TimerCallback cb) { Timestamp time(addTime(Timestamp::now(), delay)); return runAt(time, std::move(cb)); } TimerId EventLoop::runEvery(double interval, TimerCallback cb) { Timestamp time(addTime(Timestamp::now(), interval)); return timerQueue_->addTimer(std::move(cb), time, interval); } void EventLoop::cancel(TimerId timerId) { return timerQueue_->cancel(timerId); }
六、如何让EventLoop关注(监听)新的文件描述符的事件
1 void EventLoop::updateChannel(Channel* channel) 2 { 3 assert(channel->ownerLoop() == this); 4 assertInLoopThread(); 5 poller_->updateChannel(channel); 6 } 7 8 void EventLoop::removeChannel(Channel* channel) 9 { 10 assert(channel->ownerLoop() == this); 11 assertInLoopThread(); 12 if (eventHandling_) 13 { 14 assert(currentActiveChannel_ == channel || 15 std::find(activeChannels_.begin(), activeChannels_.end(), channel) == activeChannels_.end()); 16 } 17 poller_->removeChannel(channel); 18 } 19 20 bool EventLoop::hasChannel(Channel* channel) 21 { 22 assert(channel->ownerLoop() == this); 23 assertInLoopThread(); 24 return poller_->hasChannel(channel); 25 }
如何实现每个线程只能由一份EventLoop对象?
EventLoop.cc文件内的全局变量用于记录EventLoop对象指针(_thread修饰即每一个线程都有一份独立的该变量,否则该变量在线程间是共享的):
__thread EventLoop* t_loopInThisThread = 0;
利用一个静态成员函数用于断言判断线程是否已经有一个EventLoop对象:
static EventLoop* getEventLoopOfCurrentThread();
assert(EventLoop::getEventLoopOfCurrentThread() == NULL);
EventLoop loop;
assert(EventLoop::getEventLoopOfCurrentThread() == &loop);
在EventLoop对象的构造函数内调用:
if (t_loopInThisThread) { LOG_FATAL << "Another EventLoop " << t_loopInThisThread << " exists in this thread " << threadId_; } else { t_loopInThisThread = this; }
如果是同一个线程内创建了EventLoop对象两次,在第二次的构造函数中因为全局变量t_loopInThisThread已被赋值,故会强制退出第二次构造EventLoop对象失败。
EventLoop的构造函数会记住本对象所属的线程id(threadId是线程唯一的标识,类型是pid_t)。
EventLoop主要调用loop()事件循环函数,该函数不能跨线程调用。只能在创建EventLoop对象的线程中调用loop()函数。