[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所在线程的id

TimeStamp 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  一种是wakeup
        pollReturnTime_ = 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.mfbz.cn/a/617042.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

通过acl设置阻止数据包通过

实验拓扑和信息如图&#xff08;配置信息参考上一章内容&#xff09; acl设置代码 AR4 系统是视图下 acl 2000 rule 5 deny source 10.10.10.1 0 接口0视图下 数据接收时 traffic-filter inbound acl 2000 测试结果

【数据结构练习题】Map与Set——1.只出过一次的数字2.复制带随机指针的链表3.宝石与石头4.坏键盘打字

♥♥♥♥♥个人主页♥♥♥♥♥ ♥♥♥♥♥数据结构练习题总结专栏♥♥♥♥♥ ♥♥♥♥♥【数据结构练习题】堆——top-k问题♥♥♥♥♥ 文章目录 1.只出过一次的数字1.1问题描述1.2思路分析1.3绘图分析1.4代码实现2.复制带随机指针的链表2.1问题描述2.2思路分析2.3绘图分析2.4代…

Electron学习笔记(六)

文章目录 相关笔记笔记说明 七、系统5、托盘图标(1)、设置托盘图标(2)、托盘图标闪烁(3)、托盘图标菜单 6、剪切板(1)、写入剪切板(2)、读取剪切板 7、系统通知8、其他(1)、使用系统默认应用打开文件(2)、接收拖拽到窗口中的文件(3)、使用系统字体 相关笔记 Electron学习笔记&…

基于Java+SpringBoot+Mybaties-plus+Vue+elememt 驾校管理 设计与实现

一.项目介绍 系统角色&#xff1a;管理员、驾校教练、学员 管理员&#xff1a; 个人中心&#xff1a;修改密码以及个人信息修改 学员管理&#xff1a;维护学员信息&#xff0c;维护学员成绩信息 驾校教练管理&#xff1a;驾校教练信息的维护 驾校车辆管理&…

基于python的旅游爬虫可视化与实现

摘要 本项目为基于python的旅游爬虫可视化的设计与实现&#xff0c;项目以Web系统形式展示&#xff0c;利用Xpath爬虫爬取去哪儿网针对旅游业的需求&#xff0c;对国内热门旅游景点数据可视化系统&#xff0c;将爬取好的数据保存为CSV文件&#xff0c;再通过ORM框架导入MySQL数…

[muduo网络库]——muduo库Buffer类(剖析muduo网络库核心部分、设计思想)

接着之前我们[muduo网络库]——muduo库Socket类&#xff08;剖析muduo网络库核心部分、设计思想&#xff09;&#xff0c;我们接下来继续看muduo库中的Buffer类。其实Buffer在我的另一篇博客里面已经介绍过了深究muduo网络库的Buffer类&#xff01;&#xff01;&#xff01;&am…

十四、Redis Cluster集群

Redis Cluster是Redis提供的一个分布式解决方案&#xff0c;在3.0推出。Redis Cluster可以自动将数据分片分布到不同的master节点上&#xff0c;同时提供了高可用的支持&#xff0c;当某个master节点挂了之后&#xff0c;整个集群还是可以正常工作。1、为什么要用Redis Cluster…

Kafka 业务日志采集最佳实践

简介 Apache Kafka 是一个分布式流处理平台&#xff0c;主要用于构建实时数据流管道和应用程序。在收集业务日志的场景中&#xff0c;Kafka 可以作为一个消息中间件&#xff0c;用于接收、存储和转发大量的日志数据。将 Kafka 与其他系统&#xff08;如 Elasticsearch、Flume、…

AtCoder Beginner Contest 353 A~E(F,G更新中...)

A.Buildings 题意 给出若干个建筑&#xff0c;每个建筑有一个高度&#xff0c;问&#xff0c;从第二个建筑开始&#xff0c;比第一个建筑高的建筑中编号最小的是多少&#xff1f;如果不存在&#xff0c;输出-1. 分析 边输入边比较即可&#xff0c;如果循环结束还未找到&…

【muzzik 分享】Cocos 物理帧同步

# 前言 之前没研究帧同步&#xff0c;这是我前端时间没上班时边玩边搞做的 Demo 研究成果&#xff0c;总共时间一周&#xff08;实际2-3天&#xff09;&#xff0c;发布的目的也很简单&#xff0c;打破技术垄断&#xff0c;才能诞生更高端的技术成果。而且就算我没发这篇帖子&…

重生奇迹mu战士攻略有哪些

1、生命之光&#xff1a;PK前起手式&#xff0c;增加血上限。 2、雷霆裂闪&#xff1a;眩晕住对手&#xff0c;战士PK战士第一技能&#xff0c;雷霆裂闪是否使用好关系到胜负。 3、霹雳回旋斩&#xff1a;雷霆裂闪后可以选择用霹雳回旋斩跑出一定范围(因为对手下一招没出意外…

Mybatis技术内幕-基础支撑层

整体架构 MyBatis 的整体架构分为三层&#xff0c; 分别是基础支持层、核心处理层和接口层。 基础支持层 基础支持层包含整个MyBatis 的基础模块&#xff0c;这些模块为核心处理层的功能提供了良好的支撑。 解析器模块 XPathParser MyBatis提供的XPathParser 类封装了XPat…

SpringCloud使用Nacos作为配置中心实现动态数据源切换

一、Nacos-Server 了解Nacos可以直接阅读官方文档 使用Nacos&#xff0c;我们需要有Nacos-Server&#xff0c;此处就不使用官方提供的release版本了&#xff0c;而是自己编译&#xff0c;因为本来就是Java开发的&#xff0c;所以对于Javaer来说也没啥难度&#xff01; git c…

记nrm管理仓库以及发布npm包

前言 记一次在公司创建私有库以及发布npm包&#xff0c;留下个脚印 一、nrm是什么&#xff1f; nrm是 npm 镜像源管理工具&#xff0c;用于快速地在不同的 npm 源之间切换。 二、使用步骤 1.全局安装nrm 代码如下&#xff08;示例&#xff09;&#xff1a; npm install -…

Git系列:git diff使用技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

【智能算法】鹭鹰优化算法(SBOA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献5.代码获取 1.背景 2024年&#xff0c;Y Fu受到自然界中鹭鹰生存行为启发&#xff0c;提出了鹭鹰优化算法&#xff08;Secretary Bird Optimization Algorithm, SBOA&#xff09;。 2.算法原理 2.1算法思想…

Elasticsearch查看集群信息,设置ES密码,Kibana部署

Elasticsearch查看集群信息&#xff0c;设置ES密码&#xff0c;Kibana部署 查看集群信息查看节点信息查看集群健康状态查看分片信息查看其他集群信息 Kibana部署安装设置ES密码 查看集群信息 查看节点信息 curl http://127.0.0.1:9200/_cat/nodes?v 参数说明&#xff1a; ip…

OpenGL导入的纹理图片错位

在OpenGL中导入图片的纹理照片的函数为 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, p_w, p_h, 0, GL_BGR, GL_UNSIGNED_BYTE, pic_data);其中p_w, p_h为图片的宽和高&#xff0c;pic_data为指向图片存储空间的的地址(unsigned char *类型) 在OpenGL中图片默认是4字节对齐的&…

Web开发小知识点(二)

1.关于取余 我在Dart语言里&#xff08;flutter项目&#xff09; int checkNum (10 - 29) % 10; 那么checkNum等于1 但是在Vue项目里 const checkNum (10 - 29) % 10;却等于-9 语言的特性不同&#xff0c;导致结果也不同&#xff0c;如果要想和Dart保持一致&#xff0c;…

vue3.0(六) toRef,toValue,toRefs和toRow,markRaw

文章目录 toReftoValuetoRefstoRowmarkRawtoRef和toRefs的区别toRaw 和markRaw的用处 toRef toRef 函数可以将一个响应式对象的属性转换为一个独立的 ref 对象。返回的是一个指向源对象属性的 ref 引用&#xff0c;任何对该引用的修改都会同步到源对象属性上。使用 toRef 时需…