[muduo网络库]——muduo库三大核心组件之EventLoop类(剖析muduo网络库核心部分、设计思想)

接着上一节[muduo网络库]——muduo库三大核心组件之 Poller/EpollPoller类(剖析muduo网络库核心部分、设计思想),我们来剖析muduo库中最后一类核心组件,EventLoop类。
先回顾一下三大核心组件之间的关系。
在这里插入图片描述接着我们进入正题。

EventLoop

Poller封装了和事件监听有关的方法和成员,调用Poller派生类EpollPoller::poll方法,我们就可以获得发生事件的fd 及其 发生的事件。EventLoop是网络服务器中负责 循环 的重要模块,从而做到持续监听、持续获取监听结果、持续处理监听结果对应的事件。
也就是说: EventLoop起到一个驱动循环的功能,Poller负责从事件监听器上获取监听结果,Channel类将fd及其相关属性封装,并将fd及其感兴趣事件和发生的事件以及不同事件对应的回调函数封装在一起,这样在各个模块中传递更加方便。接着被EventLoop调用。
可能上面我画的图不能充分表达三者在muduo库中的角色,下面借用我在地铁站里吃闸机博主的图,可能会让大家看的更加直观。
在这里插入图片描述
在EventLoop就能够充分提现muduo库的重要思想:One Loop Per Thread
在muduo库里边有两种线程:一种里边的事件循环专门处理新用户连接(mainLoop( 也就是baseLoop)),一种里边的事件循环专门处理对应连接的所有读写事件(ioLoop)。

重要成员变量

std::unique_ptr<Poller> poller_;const pid_t threadId_; //记录当前loop所在线程的idTimeStamp pollReturnTime_; //poller返回发生事件的channels的时间点int wakeupFd_;
std::unique_ptr<Channel> wakeupChannel_;ChannelList activeChannels_;std::atomic_bool callingPendingFunctors_; std::vector<Functor> pendingFunctors_;std::mutex mutex_; 
  • poller_就不用在多说什么了,通过它会返回给EventLoop发生的事件。
  • wakeupFd_是非常重要的一个成员,与之对应的wakeupChannel_,起到了一个唤醒loop所在的线程的作用,因为当前线程主要阻塞在poll函数上,唤醒的方法时手动激活这个wakeupChannel_, 写入几个字节让Channel变为可读, 当然这个Channel也注册到Pooll中,在下面的成员函数会详细介绍它的实现。
  • threadId_创建时要保存当前时间循环所在的线程,用于之后运行时判断使用EventLoop的线程是否时EventLoop所属的线程.
  • pollReturnTime_保存poll返回的时间,用于计算从激活到调用回调函数的延迟
  • activeChannels_就是poller返回的所有发生事件的channel列表。
  • callingPendingFunctors_标识当前loop是否有需要执行的回调操作
  • pendingFunctors_存储loop需要执行的所有回调操作,避免本来属于当前线程的回调函数被其他线程调用,应该把这个回调函数添加到属于它所属的线程,等待它属于的线程被唤醒后调用,满足线程安全
  • mutex_互斥锁,用来保护vector容器的线程安全操作

重要成员函数

  • 最最最最重要的莫过于loop()
void EventLoop::loop()
{looping_ = true;quit_ = false;LOG_INFO("EventLoop %p start looping \n",this);while(!quit_){activeChannels_.clear();//监听两类fd 一种是client的fd  一种是wakeuppollReturnTime_ = poller_->poll(kPollTimeMs,&activeChannels_);for(Channel *channel : activeChannels_){//poller监听哪些channel发生事件了,然后上报给EventLoop,通知channel处理相应的事件channel->handleEvent(pollReturnTime_);}//执行当前EventLoop事件循环需要处理的回调操作/*** IO线程 mainloop accept fd <= channel  subloop* mainloop事先注册一个回调cb,需要subloop执行  * wakeup subloop后执行下面的方法 执行之前mainloop注册的cb回调* */doPendingFunctors();}LOG_INFO("EventLoop %p stop looping,\n",this);looping_ = false;
}

从代码中,我们可以看出最核心的部分就是调用了Poller的poll方法,它返回了发生的事件channel列表以及发生的时间now
接着可以看出还有一个doPendingFunctors函数

void EventLoop::doPendingFunctors()
{std::vector<Functor> functors;callingPendingFunctors_ = true; //需要执行回调//括号用于上锁 出了括号就解锁了{std::unique_lock<std::mutex> lock(mutex_);functors.swap(pendingFunctors_);}for(const Functor &functor: functors){functor();//执行当前loop需要执行的回调操作} callingPendingFunctors_ = false;
} 

实际上,这个函数就是用来执行回调的,值得注意的一点就是: 这里使用了一个比较巧妙的思想就是,使用一个局部的vectorpendingFunctors_的交换,这样就避免了因为要读取这个pendingFunctors_的时候,没有释放锁,而新的事件往里写得时候写不进去(mainloop向subloop里面写回调)。

还有一点,一开始的时候很疑惑functor();是在执行什么呢?其实在这里我们可以看出来,经过交换functor();拿到的实际上pendingFunctors_.emplace_back(cb);中的内容,执行回调。那么pendingFunctors_怎么来的?

  • 那就是runInLoop以及queueInLoop
//在当前loop中执行cb
void EventLoop::runInLoop(Functor cb)
{if(isInLoopThread())//在当前的loop线程中,执行cb{cb();}else //在非当前loop执行cb,就需要唤醒loop所在线程执行cb{queueInLoop(cb);}}void EventLoop::queueInLoop(Functor cb)
{{std::unique_lock<std::mutex> lock(mutex_);pendingFunctors_.emplace_back(cb);}if(!isInLoopThread() || callingPendingFunctors_) {wakeup();}
}

可以看出来runInLoop主要是判断是否处于当前IO线程,是则执行这个函数,如果不是则将函数加入队列queueInLoop。在queueInLoop就会把cb放入pendingFunctors_

值得注意: wakeup();这个函数:

在构造函数中已经给它注册了回调函数:

wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead,this));
wakeupChannel_->enableReading();

每一个eventloop都将监听wakeupchannel的EPOLLIN读事件了,mianreactor通过给subreactor写东西,通知其苏醒,那么handleRead里面是什么呢?

  • handleRead也是其中比较重要的一个回调了
//发送给subreactor一个读信号,唤醒subreactor
void EventLoop::handleRead()
{uint64_t one = 1;ssize_t n = read(wakeupFd_, &one, sizeof one);if(n != sizeof one){LOG_ERROR("EventLoop::handleRead() reads %d bytes instead of 8",n);}
}
  • 接着看看wakeup()源码
void EventLoop::wakeup()
{uint64_t one = 1;ssize_t n = write(wakeupFd_,&one,sizeof one);if(n != sizeof one){LOG_ERROR("EventLoop::wakeup() writes %lu bytes instead of 8 \n",n);}
}
  • 在析构的时候,关闭它
EventLoop::~EventLoop()
{wakeupChannel_->disableAll();wakeupChannel_->remove();::close(wakeupFd_);t_loopInThisThread = nullptr;
}

这就和上面提到的wakeupFd_联系起来了,
首先wakeupFd_实际上是调用eventfd,把这个wakeupFd_添加到poll中,在需要唤醒时写入8字节数据,
在构造函数中,也注册了它对应的回调函数wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead,this));
此时poll返回,执行回调函数,然后执行在pendingFunctors_中的函数。

什么时候需要唤醒呢?

if(!isInLoopThread() || callingPendingFunctors_) 

前者还是比较好理解的,One Loop Per Thread 既然不在这个loop中,那就唤醒它;后者呢?从doPendingFunctors函数中我们可以看到callingPendingFunctors_= true;时,是表明正在执行回调函数,在loop()中可以看出执行完回调,又会阻塞在poller_->poll(kPollTimeMs,&activeChannels_);,如果再次调用queueInLoop,就需要再次唤醒才能继续执行新的回调doPendingFunctors

  • 判断是否在当前线程

首先通过以下代码获取了当前的loop的线程id,

threadId_(CurrentThread::tid())

实际上是在CurrentThread类中,通过调用SYS_gettid来获得,有关于SYS_gettid在我的另一篇博客,已经给了详细的介绍Linux—C/C++编程:syscall(系统调用)、SYS_gettid在muduo库中的使用以及static_cast

然后通过isInLoopThread()

bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }

进行比较,来判断是否在当前的线程

  • 接下来还有三个回调函数
//EventLoop的方法=> poller的方法
void EventLoop::updateChannel(Channel* channel)
{poller_->updateChannel(channel);
}void EventLoop::removeChannel(Channel* channel)
{poller_->removeChannel(channel);
}bool EventLoop::hasChannel(Channel* channel)
{return poller_->hasChannel(channel);
}

这就是调用了poller_的方法。

  • 最后的最后,就是退出循环了~
void EventLoop::quit()
{quit_ = true;if(!isInLoopThread()){wakeup();}
}

当然了,不在当前线程也是需要唤醒的。

EventLoop中有很多值得学习的点,但是最巧妙的就是wakeupFd_的设计:

传统的进程/线程间唤醒办法是用pipe或者socketpair,IO线程始终监视管道上的可读事件,在需要唤醒的时候,其他线程向管道中写一个字节,这样IO线程就从IO multiplexing阻塞调用中返回。pipe和socketpair都需要一对文件描述符,且pipe只能单向通信,socketpair可以双向通信。一方面它比 pipe 少用一个 fd,节省了资源;另一方面,wakeupFd_的缓冲区管理也简单得多,全部buffer只有定长8 bytes,不像 pipe 那样可能有不定长的真正 buffer。muduo库也没有采用生产者消费者的模型,采用了wakeupFd_这种巧妙的思想,在今后的学习中,我们也可以进一步的使用它。

最后附上代码地址:https://github.com/Cheeron955/mymuduo/tree/master

好了,关于muduo库三大核心组件之EventLoop类就到此结束了,三大核心组件我们都一一梳理介绍了,希望能够帮助到大家,接下来会对其余类进行一个梳理~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/687446.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】CentOS环境搭建-快速升级G++版本

【C】CentOS环境搭建-快速升级G版本 1. 安装CentOS的软件集仓库&#xff1a;2. 安装你想要的devtoolset版本&#xff0c;例如devtoolset-9&#xff1a;3. 启用新版本的编译器&#xff1a;4. 检查G版本&#xff1a; 在CentOS系统中升级G编译器通常涉及使用devtoolset或者SCL&…

gin框架学习笔记(二) ——相关数据与文件的响应

前言 在看是今天的内容之前&#xff0c;我们收先来探究一下&#xff1a;什么是Web应用工作的原理&#xff1f;当然这个问题其实论述起来是很麻烦的&#xff0c;但是我们将它无限的缩小&#xff0c;其实可以简化为一个C/S模型&#xff0c;客户端(Client)负责发送请求&#xff0…

docker 容器无法直接读取宿主机文件

最近一个需求, 要在后端直接使用代码直接生成 pdf 文档, 由于使用的 apache 的工具包, 该工具包无法直接解析中文字体, 需要导入外部 中文插件包, 相关代码如下: PDPage page new PDPage(PDRectangle.A4);document.addPage(page);PDFont fontFile PDType0Font.load(document…

C++ VScode: launch: program ...... dose not exist

VScode: launch: program … dose not exist 介绍 参考VS Code 配置 C/C 编程运行环境&#xff08;保姆级教程&#xff09;教程配置了VSCode。在配置launch.json适用多个.c 文件编译时&#xff0c;弹出下面错误。 原因和解决方法 是task.json 默认配置的问题。 默认的 cwd参…

Redis20种使用场景

Redis20种使用场景 1缓存2抽奖3Set实现点赞/收藏功能4排行榜5PV统计&#xff08;incr自增计数&#xff09;6UV统计&#xff08;HeyperLogLog&#xff09;7去重&#xff08;BloomFiler&#xff09;8用户签到&#xff08;BitMap&#xff09;9GEO搜附近10简单限流11全局ID12简单分…

AVL树的旋转

目录 1.平衡因子 2.旋转 a.节点定义 b.插入 插入 平衡因子更新 旋转 左单旋 右单旋 右左双旋 左右双旋 3.AVL树的验证 1.平衡因子 我们知道搜索二叉树有缺陷&#xff0c;就是不平衡&#xff0c;比如下面的树 什么是搜索树的平衡&#xff1f;就是每个节点的左右子树的…

机器学习——6.模型训练案例: 预测儿童神经缺陷分类TD/ADHD

案例目的 有一份EXCEL标注数据&#xff0c;如下&#xff0c;训练出合适的模型来预测儿童神经缺陷分类。 参考文章&#xff1a;机器学习——5.案例: 乳腺癌预测-CSDN博客 代码逻辑步骤 读取数据训练集与测试集拆分数据标准化数据转化为Pytorch张量label维度转换定义模型定义损…

Hive的文件存储格式

TEXTFILE 文本文件,默认的文件存储格式,内容是可以直接查看,用来保存非结构化数据; 特点:文本文件的格式一旦定义就无法改变. 除TEXTFILE外,其他文件存储格式的表不能直接从本地文件导入数据,数据要先导入到textfile格式的表中, 然后再从表中用insert导入SequenceFile,RCFile,…

权限束缚术--权限提升你需要知道这些

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文主要对渗透测试中权限提升的一些基础知识进行整理 并不包含权限提升的具体操作 适合要入门权限提升的朋友 提权的重要性 我们在渗透网站时&#xff0c;我们往往会拿到一些权限&#xff0c;但是我们的权限有…

C语言什么是散列法?

一、问题 什么是散列法&#xff1f; 二、解答 散列法是⼀种将字符组成字符串&#xff0c;转换为固定长度&#xff08;⼀般是更短长度&#xff09;的数值或索引值的⽅法&#xff0c;也叫哈希法&#xff0c;⼜可以称为杂凑法或关键码⼀地址转换法。 那么&#xff0c;通过散列函数…

Aapache Tomcat AJP 文件包含漏洞(CVE-2020-1938)

1 漏洞描述 CVE-2020-1938 是 Apache Tomcat 中的一个严重安全漏洞&#xff0c;该漏洞涉及到 Tomcat 的 AJP&#xff08;Apache JServ Protocol&#xff09;连接器。由于 AJP 协议在处理请求时存在缺陷&#xff0c;攻击者可以利用此漏洞读取服务器上的任意文件&#xff0c;甚至…

融入新科技的SLM27211系列 120V, 3A/4.5A高低边高频门极驱动器兼容UCC27284,MAX15013A

SLM27211是高低边高频门极驱动器&#xff0c;集成了120V的自举二极管&#xff0c;支持高频大电流的输出&#xff0c;可在8V~17V的宽电压范围内驱动MOSFET&#xff0c;独立的高、低边驱动以方便控制&#xff0c;可用于半桥、全桥、双管正激和有源钳位正激等拓。有极好的开通、关…