Muduo奉行的是每个one loop per thread,意思是每个线程只有一个EventLoop对象。在Muduo中,称创建了EventLoop对象的线程是IO线程。
我主要关注大体框架,有些细节暂时不关注。首先看看构造函数(下列源码源自Muduo的教程示例)
EventLoop::EventLoop()
: looping_(false),
quit_(false),
callingPendingFunctors_(false),
threadId_(CurrentThread::tid()),
poller_(new EPoller(this)),
timerQueue_(new TimerQueue(this)),
wakeupFd_(createEventfd()),
wakeupChannel_(new Channel(this, wakeupFd_))
{
LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
if (t_loopInThisThread)
{
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
}
else
{
t_loopInThisThread = this;
}
wakeupChannel_->setReadCallback(
boost::bind(&EventLoop::handleRead, this));
// we are always reading the wakeupfd
wakeupChannel_->enableReading();
}
第一个问题,Muduo如何保证每个线程只能创建一个EventLoop对象?
原因是Eventloop使用了TLS(线程局部存储)
__thread EventLoop* t_loopInThisThread = 0;
EventLoop对象在构造时,会去检查本线程是已经创建了其他的EventLoop对象。如果没有创建EventLoop对象,那么t_loopInThisThread将是一个空指针,然后将本EventLoop对象的指针赋给t_loopInThisThread。如果本线程再次创建EventLoop对象时,去检测t_loopInThisThread,发现不为空,说明已经创建了其他对象,就进行终止日志输出。
由于是TLS,所以每个线程只会关注每个线程的创建情况,如果是全局变量,那么多个线程之间无法区分创建情况(个人理解,希望指正)
第二个问题,如何判断一个线程是否是IO线程?
比对当前线程的线程ID是否是EventLoop对象的创建线程ID。对象在创建时,保存了线程ID
bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
第三个问题,loop成员函数做什么?
首先看代码,代码如下。
void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread();
looping_ = true;
quit_ = false;
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
(*it)->handleEvent(pollReturnTime_);
}
doPendingFunctors();
}
LOG_TRACE << "EventLoop " << this << " stop looping";
looping_ = false;
}
loop之前的一些常规检查就不必多说。EventLoop对象有一个数据成员activeChannels_。定义如下,用来维护活跃的Channel对象,活跃是指可读、可写、或者出错。篇幅有限,Channel的分析就不在这篇博客中分享了。
typedef std::vector ChannelList;
ChannelList activeChannels_;
loop每一次迭代时,首先清空之前的activeChannels_。然后使用poller_调用poll,获取最新的活跃Channel,添加至activeChannels_。然后遍历activeChannels_,执行channel对象的handleEvent成员函数。最后执行doPendingFunctors,其中保存着希望在IO线程中执行的函数。
poller就是IO复用函数的封装,可以是select,poll,epoll。所以loop函数大部分时间会阻塞在poll函数上。这也符合非阻塞IO模型的逻辑。
第四个问题,如何将用户的函数放在IO线程中执行?
使用runInLoop成员函数
void EventLoop::runInLoop(const Functor& cb)
{
if (isInLoopThread())
{
cb();
}
else
{
queueInLoop(cb);
}
}
首先判断当前线程是否是IO线程,如果当前线程就是IO线程,那么就直接执行需要执行的函数。如果是其他线程,那么就将需要执行的函数放在队列中,等待执行,使用queueInLoop函数。
void EventLoop::queueInLoop(const Functor& cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(cb);
}
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}
由于可能出现多个线程同时调用queueInloop,所以向队列中添加时,需要加锁,保证线程安全。然后唤醒loop,因为上面说了,loop大部分时间会阻塞在poll上,直到有描述符发生事件。但是我此时有线程希望将函数放在io线程中执行,那么怎么办呢?这里Muduo使用了大多数网络库使用的技巧,向一个描述符中写一个字节(这个描述符在EventLoop对象构造时已经被添加关注),以便程序中poll中返回。返回后就可以执行doPendingFunctors函数了。这个函数就是处理希望在IO线程中执行的函数,使用了一个队列保存回调函数。实现如下
void EventLoop::doPendingFunctors()
{
std::vector functors;
callingPendingFunctors_ = true;
{
MutexLockGuard lock(mutex_);
functors.swap(pendingFunctors_);
}
for (size_t i = 0; i < functors.size(); ++i)
{
functors[i]();
}
callingPendingFunctors_ = false;
}
为了减少对临界的占用,先将队列中的值保存出来,然后执行。
整体流程图如下
这就是EventLoop大致执行的流程,能够对Muduo有一定了解,当然其中也包含一些的细节部分去考究。