先简单说一下,因为我参考的是flamingo的代码,会和muduo有一点点出入,但是基本是差不多,因为前者是基于后者开发的,可能有一点点改动。
EventLoop是muduo里比较核心的类吧,因为它是实现Reactor模式的核心,什么是Reactor模式,其实很多文章和书籍都有讲,我也打算抽空写一篇博客,来总结下自己的心得,至少现在没有总结,哈哈~~。
其实如果认真读一下EventLoop里的Loop循环的话,就可以很好的了解Reactor模式了,
不过还是先从成员开始吧!
private:
typedef std::vector<Channel*> ChannelList;
bool looping_; /* atomic */
bool quit_; /* atomic and shared between threads, okay on x86, I guess. */
bool eventHandling_; /* atomic */
bool callingPendingFunctors_; /* atomic */
const std::thread::id threadId_;//对应的线程id号
Timestamp pollReturnTime_;
std::shared_ptr<Poller> poller_;//初始化默认创建EpollPoller
std::shared_ptr<TimerQueue> timerQueue_;
int64_t iteration_;
SOCKET wakeupFd_; //TODO: 这个fd什么时候释放?,在EventLoop被析构的时候会释放,但是确实没有找到相应调用析构的代码。
// unlike in TimerQueue, which is an internal class,
// we don't expose Channel to client.
std::shared_ptr<Channel> wakeupChannel_;
// scratch variables
ChannelList activeChannels_;//epoll事件会被压入这个vector
Channel* currentActiveChannel_;//被用来遍历,上一行的vector
std::mutex mutex_;//用来保护下面那行的执行
std::vector<Functor> pendingFunctors_; // Guarded by mutex_
Functor frameFunctor_;
};
注意!为了减少行数,我把#indef win32给删掉了。
先简单说一下4个bool型的变量:
looping_代表是否开始循环。
quit_代表是否退出。
eventHandling_代表是否在处理事件。
callingPendingFunctors_代表是否在处理被push的待执行函数(此处后面会说一下)。
说一下这个东西
SOCKET wakeupFd_;//TODO: 这个fd什么时候释放?,在EventLoop被析构的时候会释放,但是确实没有找到相应调用析构的代码。
这个socket使用来唤醒的,有一个wakeup函数,会向这个socket写入一个uint64_t one = 1;来唤醒它。不过这个释放,确实没有找到具体的实现位置,它会在EventLoop被析构的时候被释放,但是muduo代码里好像没有析构EventLoop的代码,而且EventLoopThread和EventLoopThreadPool持有EventLoop的方式是使用EventLoop*,也不是使用智能指针,不会自动清理,其实算是一个会造成内存泄漏的问题点吧。不过muduo框架也没有重复的去创建EventLoop的过程,而是在程序一开始运行就初始化好了,所以也不会造成别的问题吧,只要客户代码不反复的去创建TcpServer,那就不会出现问题,所以这是需要注意的一点!在flamingo里面,chatServer,你可以就把它当作是TcpServer,是使用单例模式创建的,所以就避免了这个问题。
后续做一个小测试,反复创建看看效果如何!
后面也没有特别需要解释说明的成员了,基本在注释里就写好了。
我们来看看它的构造
EventLoop::EventLoop()
: looping_(false),
quit_(false),
eventHandling_(false),
callingPendingFunctors_(false),
threadId_(std::this_thread::get_id()),
timerQueue_(new TimerQueue(this)),
iteration_(0L),//LONG型
currentActiveChannel_(NULL)
{
createWakeupfd();
#ifdef WIN32
wakeupChannel_.reset(new Channel(this, wakeupFdSend_));
poller_.reset(new SelectPoller(this));
#else
wakeupChannel_.reset(new Channel(this, wakeupFd_));
poller_.reset(new EPollPoller(this));//默认使用Epoll,用这个Eventloop初始化EpollPoller
#endif
if (t_loopInThisThread)
{
LOGF("Another EventLoop exists in this thread ");
}
else
{
t_loopInThisThread = this;
}
wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead, this));
// we are always reading the wakeupfd
wakeupChannel_->enableReading();
//std::stringstream ss;
//ss << "eventloop create threadid = " << threadId_;
//std::cout << ss.str() << std::endl;
}
接下来看看最主要的loop
void EventLoop::loop()
{
//assert(!looping_);
assertInLoopThread();
looping_ = true;
quit_ = false; // FIXME: what if someone calls quit() before loop() ?
LOGD("EventLoop 0x%x start looping", this);
while (!quit_)
{
timerQueue_->doTimer();
activeChannels_.clear();
//return一个事件戳,并将收到了epoll事件,放入activeChannels_中,由后面的代码执行
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
//if (Logger::logLevel() <= Logger::TRACE)
//{
printActiveChannels();
//}
++iteration_;
// TODO sort channel by priority
eventHandling_ = true;//正在处理事件
for (const auto& it : activeChannels_)
{
currentActiveChannel_ = it;
currentActiveChannel_->handleEvent(pollReturnTime_);//处理事件
}
currentActiveChannel_ = nullptr;
eventHandling_ = false;
doPendingFunctors();
if (frameFunctor_)
{
frameFunctor_();
}
}
LOGD("EventLoop 0x%0x stop looping", this);
looping_ = false;
std::ostringstream oss;
oss << std::this_thread::get_id();
std::string stid = oss.str();
LOGI("Exiting loop, EventLoop object: 0x%x , threadID: %s", this, stid.c_str());
}
while循环判断的标志时quit_,前一篇文章也说了,通过修改quit_为true来使得EventLoop结束。
我们主要关注while内的循环。
activeChannels_.clear();
//return一个事件戳,并将收到了epoll事件,放入activeChannels_中,由后面的代码执行
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
首先是这个,它清空了activeChannels_,然后通过poll完成两件事情,第一返回时间戳,第二epoll将读取到事件的Channel,记录到activeChannels_。在flamingo中,EpollPoller类会维持一个。muduo也是如此。
typedef std::vector<struct epoll_event> EventList;
int epollfd_;
EventList events_;//eventslist是一个事件数组,用vector存储,元素类型为epoll_event的结构体
事件会以struct epoll_event的形式被记录到events_中。
eventHandling_ = true;//正在处理事件
for (const auto& it : activeChannels_)//遍历vector
{
currentActiveChannel_ = it;
currentActiveChannel_->handleEvent(pollReturnTime_);//处理事件
}
currentActiveChannel_ = nullptr;
eventHandling_ = false;
这个过程就比较清晰了,将正在处理事件的标志置为true,然后遍历activeChannels_,调用Channel的handleEvent函数处理事件。
接下来是,dopendingFunctors。
void EventLoop::doPendingFunctors()
{
std::vector<Functor> functors;
callingPendingFunctors_ = true;
{
std::unique_lock<std::mutex> lock(mutex_);//加锁,防止这时候再push_back
functors.swap(pendingFunctors_);//交换内容
}
for (size_t i = 0; i < functors.size(); ++i)
{
functors[i]();
}
callingPendingFunctors_ = false;
}
这里新开了一个std::vector functors,并做了交换,防止了因为函数再执行,阻塞别的线程push_back,这个设计还是不错的,具体可以参考一下这里。另外注意到了,这个大括号的使用,我简单试了一下,发现了以前不知道的新大陆。
相信能看出来了吧,在doPendingFunctors里,通过大括号结束了unique_lock的生命周期,从而释放了mutex_。vector的swap其实是指针的交换,vector的迭代器实现是指针,所以其实消耗非常低,这应该也是作者使用vector容器来存放的原因把。
定义
SOCKET wakeupFd_; //EventLoop被析构的时候被释放掉。
初始化
bool EventLoop::createWakeupfd()
{
//Linux上一个eventfd就够了,可以实现读写
wakeupFd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (wakeupFd_ < 0)
{
//让程序挂掉
LOGF("Unable to create wakeup eventfd, EventLoop: 0x%x", this);
return false;
}
return true;
}
关于这个evenfd,它是一个专门用来做通知用的文件描述符。用来做唤醒进程用。
void EventLoop::runInLoop(const Functor& cb)
{
if (isInLoopThread())//如果是本线程,则直接运行,否则加入队列,等待运行
{
cb();
}
else
{
queueInLoop(cb);
}
}
void EventLoop::queueInLoop(const Functor& cb)//将函数放入队列,等待执行,或者立即执行
{
{
std::unique_lock<std::mutex> lock(mutex_);
pendingFunctors_.push_back(cb);
}
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}
第一个函数,其实就是判断线程是不是当前线程,如果是的话就立即执行,如果不是就压入pendingFunctors中。
第二个函数就是具体压入的步骤了。