热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

muduo源码分析之EventLoop、Channel、Poller的实现

作者一直强调的一个概念叫做oneloopperthread,撇开多线程不谈,本篇博文将学习,怎么将传统的IO复用pollepoll封装到C++类中。1.IO复用复习使用p

作者一直强调的一个概念叫做one loop per thread,撇开多线程不谈,本篇博文将学习,怎么将传统的I/O复用poll/epoll封装到C++ 类中。

1.I/O复用

复习使用poll/epoll进行I/O复用的一些编程内容。

使用poll

对于一个文件描述符fd来说,我们将通过struct pollfd来设置我们关注的事件event,并在通过poll调用返回获取活跃的事件revent。比如说(伪代码):

struct pollfd pfds[2];
fd1 = GET_FD();
fd2 = GET_FD();

pfds[0].fd=fd1;
pfds[0].events=POLLIN;
//fd2 is the same
while(true)
{
ret=poll(pfds,pfds中关心的描述符数目,TIMEOUT);
if(pfds[0].revents & POLLOUT)
{
///
}
///
}

使用epoll

epoll就要复杂一些了,主要由epoll_createepoll_ctlepoll_wait函数组成,比如说(伪代码):

epollfd=epoll_create()
struct epoll_event ev;
ev.event=EPOLL_IN;
ev.data.fd=fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
while(true)
{
epoll_wait();
handle_event();
}

2.什么都不做的EventLoop

本文代码参见https://github.com/chenshuo/recipes

one loop per thread表示每个线程只能有一个EventLoop对象,怎么来保证呢?
作者用__thread关键字修饰的t_loopInThisThread来存储对象指针(它的值在多线程环境下互不干扰),每当EventLoop进行构造的时候,判断该指针是否为NULL也就是在之前有没有被初始化过。

// reactor/s00/EventLoop.cc

/*__thread关键字 各个线程之间的值相互不干扰*/
__thread EventLoop* t_loopInThisThread = 0;

EventLoop::EventLoop()
: looping_(false),
threadId_(CurrentThread::tid())
{
LOG_TRACE <<"EventLoop created " <<this <<" in thread " < /*t_loopInThisThread不为空表示当前线程创建了EventLoop对象*/
/*one thread per loop表示一个线程只能有一个EventLoop对象*/
if (t_loopInThisThread)
{

LOG_FATAL <<"Another EventLoop " < <<" exists in this thread " < }
else
{
/*记录该线程的EventLoop对象*/
t_loopInThisThread = this;
}
}

对于一个简单的EventLoop来说,poll调用封装在EventLoop::loop()中,比如说:

void EventLoop::loop()
{
assert(!looping_);
/*确保EventLoop在创建它的线程中loop调用*/
assertInLoopThread();
/*等待5秒的poll调用*/
::poll(NULL, 0, 5*1000);
LOG_TRACE <<"EventLoop " <<this <<" stop looping";

}

3.Reactor关键结构

主要介绍Channel类(用于对描述符及事件的封装和事件的分发)Poller类(主要对poll / epoll调用的封装,但只是获取活跃事件并不负责事件分发)

我一直思考的问题就在于ChannelPollerEventLoop是如何协调工作的,所以,先从一个示例看起。

// reactor/s01/test3.cc

muduo::EventLoop* g_loop;

void timeout()
{
printf("Timeout!\n");
g_loop->quit();
}

int main()
{
muduo::EventLoop loop;//EventLoop对象
g_loop = &loop;


//关心的描述符
int timerfd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
//创建channel对象,保存一份EventLoop的指针
muduo::Channel channel(&loop, timerfd);
//设置描述符可读时执行的回调函数
channel.setReadCallback(timeout);
//该函数用于设置关心的事件event
//并且会依次调用Channel::update ---> EventLoop::updateChannel --->Poller::updateChannel
channel.enableReading();

struct itimerspec howlong;
bzero(&howlong, sizeof howlong);
howlong.it_value.tv_sec = 5;
::timerfd_settime(timerfd, 0, &howlong, NULL);

//loop循环
loop.loop();

::close(timerfd);
}

程序执行的结果是5秒之后timefd可读将执行回调函数,并关闭loop。

现在我们来阅读相关源码,从上面的注释,首先,需要构造一个Channel对象:

Channel::Channel(EventLoop* loop, int fdArg)
: loop_(loop),//保存EventLoop对象的指针
fd_(fdArg),//描述符
events_(0),//关心的事件
revents_(0),//活跃的事件
index_(-1)//index_用于记录当前fd在pollfd数组中的位置
{
}

之后设置回调,这里用到boost::function和boost::bind就不再详细描述。

enableReading干了两件事情:
1、设置关心的事情为POLL_IN,事件可读。
2、将fd添加到pollfd数组中。
第2步将依次调用Channel::update ---> EventLoop::updateChannel --->Poller::updateChannel
分别来看看这些函数:

void Channel::update()
{
loop_->updateChannel(this);//这里将channel对象的指针传到了EventLoop里面
/*loop->updateChannel--->Poller::updateChannel*/
}
/////////////////////////////////////////////////////////////////////////////
void EventLoop::updateChannel(Channel* channel)
{
assert(channel->ownerLoop() == this);
assertInLoopThread();
//poller_是EventLoop中Poller类型的成员通过EventLoop的构造函数初始化。
poller_->updateChannel(channel);//将channel对象的指针传给了Poller对象
}
/////////////////////////////////////////////////////////////////////////
void Poller::updateChannel(Channel* channel)
{
/*与其ownerLoop在同一个线程*/
assertInLoopThread();
LOG_TRACE <<"fd = " <fd() <<" events = " <events();
if (channel->index() <0) {//添加时Channel::index为初始化值-1
/*添加一个新的*/
/*每次都在尾部添加*/
// a new one, add to pollfds_
assert(channels_.find(channel->fd()) == channels_.end());
struct pollfd pfd;//pollfd结构体
//进行设置
pfd.fd = channel->fd();
pfd.events = static_cast<short>(channel->events());
pfd.revents = 0;
//pollfds_是Poller类中的成员是一个pollfd类型的 vector,充当poll调用的第一个参数
pollfds_.push_back(pfd);
int idx = static_cast<int>(pollfds_.size())-1;//重新计算索引,方便下次更新时快速定位
channel->set_index(idx);//类似于设置上下文
channels_[pfd.fd] = channel;//channels_是fd和Channel的一个map
} else {
// update existing one
// 更新fd,由于channel保存有index,因此访问的时间效率为O(1)
// 可能修改event后进入该分分支。
assert(channels_.find(channel->fd()) != channels_.end());
assert(channels_[channel->fd()] == channel);
int idx = channel->index();
assert(0 <= idx && idx <static_cast<int>(pollfds_.size()));
struct pollfd& pfd = pollfds_[idx];
assert(pfd.fd == channel->fd() || pfd.fd == -1);
pfd.events = static_cast<short>(channel->events());
pfd.revents = 0;
if (channel->isNoneEvent()) {
// ignore this pollfd
pfd.fd = -1;
}
}
}

到目前位置,我们已经将我们关心的描述符及事件都设置好了,现在就需要进行真正的poll调用了,也就是loop循环

void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread();
looping_ = true;
quit_ = false;

while (!quit_)
{
activeChannels_.clear();
//在这个poll调用中封装了真正的poll(2)
poller_->poll(kPollTimeMs, &activeChannels_);
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
(*it)->handleEvent();
}
}

LOG_TRACE <<"EventLoop " <<this <<" stop looping";
looping_ = false;
}

EventLoop类中保存了一个vector类型的activeChannels,用来获取活跃的事件,因为从上文Poller类中的一个channel变量(map)就可以看出Channel和fd是可以一一对应的。

再来看看Poller封装的poll调用。

Timestamp Poller::poll(int timeoutMs, ChannelList* activeChannels)
{
// XXX pollfds_ shouldn't change
int numEvents = ::poll(&*pollfds_.begin(), pollfds_.size(), timeoutMs);//poll调用
LOG_INFO< Timestamp now(Timestamp::now());
if (numEvents > 0) {
LOG_TRACE <" events happended";
/*填写活动事件*/
fillActiveChannels(numEvents, activeChannels);//将活跃的channel添加到avtiveChannel中去
//并且设置本channel对象的revent
} else if (numEvents == 0) {
LOG_TRACE <<" nothing happended";
} else {
LOG_INFO<<"time out";
LOG_SYSERR <<"Poller::poll()";
}
return now;
}

最后,看到一个for循环,因为活跃的描述符/Channel可能很多,muduo 的做法,是现将所有活跃的描述符/Channel用activeChannel管理起来再迭代一次,对于每个Channel调用它们的handleEvent方法,该方法就根据设置的revent调用之前设置的回调方法。

void Channel::handleEvent()
{
/*活动事件回调*/
if (revents_ & POLLNVAL) {
LOG_WARN <<"Channel::handle_event() POLLNVAL";
}

if (revents_ & (POLLERR | POLLNVAL)) {
if (errorCallback_) errorCallback_();
}
if (revents_ & (POLLIN | POLLPRI | POLLRDHUP)) {
if (readCallback_) readCallback_();
}
if (revents_ & POLLOUT) {
if (writeCallback_) writeCallback_();
}
}

4.参考

1.Linux多线程服务端编程 使用MuduoC++网络库
2.http://blog.csdn.net/jnu_simba/article/details/14486661


推荐阅读
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • Iamtryingtocreateanarrayofstructinstanceslikethis:我试图创建一个这样的struct实例数组:letinstallers: ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
author-avatar
开发者小白
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有