muduo net库学习笔记4——事件驱动循环EventLoop、runInLoop和queueInLoop及对应唤醒

2023-11-17

首先总体情况:
每个muduo网络库有一个事件驱动循环线程池 EventLoopThreadPool,线程池用在事件驱动循环上层,也就是事件驱动循环是线程池中的一个线程

  • 每个TcpServer对应一个事件驱动循环线程池
  • 每个线程池中有多个事件驱动线程EventLoopThread
  • 每个线程运行一个Eventloop事件循环
  • 每个EventLoop事件循环包含一个IO复用Poller, 一个计时器队列TimerQueue
  • 每个Poller监听多个Channel, 也就对应一个TcpConnection或者监听套接字,TimeQueue其实也是一个Channel
  • 在poll返回后处理激活队列中Channel的过程是同步的,也就是一个一个调用回调函数
  • 每个回调函数是在EventLoop所在线程执行
  • 调用回调函数的线程和事件驱动主循环所在线程是同一个,也就是同步执行回调函数
  • 所有激活的Channel回调结束后,EventLoop继续让Poller监听

调用回调函数的过程中是同步的,所以如果回调函数执行时间很长,那么这个EventLoop所在线程就会等待很久之后才再次Poll

多线程体现在EventLoop的上层,即在EventLoop上层有一个线程池,线程池中每一个线程运行一个EventLoop, 也就是Reactor+线程池的设计模式

EventLoop成员变量

在这里插入图片描述创建时要保存当前时间循环所在的线程,用于之后运行时判断使用EventLoop的线程是否时EventLoop所属的线程threadId_;保存poll返回的时间,用于计算从激活到调用回调函数的延迟pollReturnTime_poller_时io多路复用,timerQueue_定时器队列
下一个wakeupFd_用于唤醒当前线程,因为当前线程主要阻塞在poll函数上,唤醒的方法时手动激活这个wakeupChannel_, 写入几个字节让Channel变为可读, 当然这个Channel也注册到Pooll中

最后一个变量std::vector<Functor> pendingFunctors_是一个任务容器,存放的是将要执行的回调函数避免本来属于当前线程的回调函数被其他线程调用,应该把这个回调函数添加到属于它所属的线程,等待它属于的线程被唤醒后调用,满足线程安全
将某个对象暴露给这是非常不安全的,万一这个线程不小心析构了这个对象,而这个对象所属的那个线程正要访问这个对象(例如调用这个对象的接口),这个线程就会崩溃,因为它访问了一个本不存在的对象(已经被析构)

为了解决这个问题即事件循环不属于当前线程,就需要尽量将对这个对象的操作移到它所属的那个线程执行(这里是调用这个对象的接口)。因为每个对象都有它所属的事件驱动循环EventLoop,这个EventLoop通常阻塞在poll上。可以保证的是EventLoop阻塞的线程就是它所属的那个线程,所以调用poll的线程就是这个对象所属的线程。这就可以让poll返回后再执行想要调用的函数(??),但是需要手动唤醒poll,否则一直阻塞在那里会耽误函数的执行。

出现事件驱动循环不属于当前线程的例子:

1.客户端close连接,服务器端某个Channel被激活,原因为EPOLLHUP
2.Channel调用回调函数,即TcpConnection的handleClose
3.handleClose调用TcpServer为handleClose提供的回调函数removeConnection
4.此时执行的是TcpServer的removeConnection函数,

这就导致将TcpServer暴露给了TcpConnection所在线程
TcpServer要将这个关闭的TcpConnection从tcp map中删除,就需要调用自己的另一个函数removeConnectionInLoop
为了实现线程安全性
要让 removeConnectionInLoop在TcpServer自己所在线程执行,
需要先把这个函数添加到队列中存起来,等到回到自己的线程在执行

runInLoop中的queueInLoop函数就是将这个函数存起来
在这里插入图片描述
当然,如果调用runInLoop所在线程和事件驱动循环线程是同一个线程,那么可以直接调用回调函数

runInLoopqueueInLoop

在这里插入图片描述

代码片段1:EventLoop::runInLoop()
文件名:EventLoop.cc
代码逻辑:判断是否处于当前IO线程,是则执行这个函数,如果不是则将函数加入队列
// 在IO线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
  if (isInLoopThread())
  {
    // 如果是当前IO线程调用runInLoop,则同步调用cb
    cb();
  }
  else
  {
    // 如果是其它线程调用runInLoop,则异步地将cb添加到队列
    queueInLoop(cb);
  }
}

这个队列就是EventLoop类的最后一个变量pendingFunctors_,将cb放入队列后,我们还需要在必要的时候唤醒IO线程来处理,因为EventLoop通常阻塞在poll上, 所以添加到pendingFunctors_后需要手动唤醒它,不然就一直阻塞在poll,会耽误函数的执行

代码片段2:EventLoop::queueInLoop()
文件名:EventLoop.cc

void EventLoop::queueInLoop(const Functor& cb)
{
  // 把任务加入到队列可能同时被多个线程调用,需要加锁
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);
  }//这里的大括号是语句块,把里面的变量作为临时变量处理

  // 必要的时候有两种情况:
  // 1.如果调用queueInLoop()的不是IO线程,需要唤醒
  // 2.如果在IO线程调用queueInLoop(),且此时正在调用pending functor,需要唤醒
  // 即只有在IO线程的事件回调中调用queueInLoop()才无需唤醒
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

关于唤醒时间??

代码片段3:EventLoop::loop()部分
文件名:EventLoop.cc

while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    // 执行pending Functors_中的任务回调
    // 这种设计使得IO线程也能执行一些计算任务,避免了IO线程在不忙时长期阻塞在IO multiplexing调用中
    doPendingFunctors();
  }

在这里插入图片描述
在这里插入图片描述

doPendingFunctors()函数⭐

EventLoop::doPendingFunctors()不是简单地在临界区依次调用Functor,而是把回调列表swap()到局部变量functors中,这样做,一方面减小了临界区的长度(不会阻塞其他线程调用queueInLoop()),另一方面避免了死锁(因为Functor可能再调用queueInLoop())。

代码片段4:EventLoop::doPendingFunctors()
文件名:EventLoop.cc

void EventLoop::doPendingFunctors()
{
  std::vector<Functor> functors;
  callingPendingFunctors_ = true;

  // 把回调列表swap()到局部变量functors中
  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }

  // 依次执行回调列表中的函数
  for (size_t i = 0; i < functors.size(); ++i)
  {
    functors[i]();
  }
  callingPendingFunctors_ = false;
}

唤醒方式

传统的进程/线程间唤醒办法是用pipe或者socketpair,IO线程始终监视管道上的可读事件,在需要唤醒的时候,其他线程向管道中写一个字节,这样IO线程就从IO multiplexing阻塞调用中返回。pipe和socketpair都需要一对文件描述符,且pipe只能单向通信,socketpair可以双向通信。

muduo所采用的一种高效的进程/线程间事件通知机制:eventf

// 头文件
#include <sys/eventfd.h> 

// 为事件通知创建文件描述符
// 参数initval表示初始化计数器值
// 参数flags可取EFD_NONBLOCK非阻塞、EFD_CLOEXEC(设置close-on-exec属性,调用exec时会自动close)、EFD_SEMAPHORE 。。。
int eventfd(unsigned int initval, int flags);

它的高效体现在:一方面它比 pipe 少用一个 fd,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,全部buffer只有定长8 bytes,不像 pipe 那样可能有不定长的真正 buffer。

可以把这个eventfd添加到poll中,在需要唤醒时写入8字节数据,此时poll返回,执行回调函数,然后执行在pendingFunctors_中的函数。❗❗❗

代码片段5:EventLoop::wakeup()
文件名:EventLoop.cc

void EventLoop::wakeup()
{
  uint64_t one = 1;
  // 向wakupFd_中写入8字节从而唤醒,wakeupFd_即eventfd()所创建的文件描述符
  ssize_t n = ::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

muduo net库学习笔记4——事件驱动循环EventLoop、runInLoop和queueInLoop及对应唤醒 的相关文章

随机推荐

  • ArcGIS 10.8打开后显示loading document后闪退

    安装ArcGis完成后 打开ArcMap 卡在Loading Document后 闪退的问题 为何为肖的博客 CSDN博客 arcmap加载文档后闪退b同事新装ArcGis10 5后出现这个情况 联想以前编译Qgis时python环境变量的
  • 由一个例子,介绍linux目录的多用户及其读写可执行权限

    例子 查看文件的读写权限以及所属用户 查看目录的读写权限以及所属用户 目录读写权限默认为755即rwxr xr x 文件读写权限默认为744即rwxrr 目录和子目录的读写权限特点 例子 ls ld home1 drwxrwxrwx 39
  • 模板小程序是什么?怎么选择?

    模板小程序是指第三方服务商基于某个模板开发的小程序 这个模板可以根据开发者的需求进行定制 包括页面设计 功能模块 数据管理等方面 开发者可以在模板基础上进行自主开发 实现自己的需求和功能 模板小程序的优点是开发周期短 价格相对较低 一般是直
  • 白夜追凶 :手 Q 图片的显示和发送逻辑

    欢迎大家前往腾讯云社区 获取更多腾讯海量技术实践干货哦 作者 陈舜尧 导语 这张图片在快捷发图栏背景是黑色的 为啥发到AIO 会话窗口 里背景就变成白的了 通过一个bug单 对黑白背景问题跟进的过程中发现了手q中很多奇怪的表现 一层层看代码
  • SpringBoot支付宝接入实战

    文章目录 支付宝支付后端实战 基于SpringBoot 一 支付宝支付介绍及接入指引 1 支付宝开放能力介绍 1 能力地图 2 电脑网站支付产品 2 接入准备 1 开放平台帐号注册 2 常规接入流程 3 使用沙箱 二 项目的环境准备 1 框
  • js小案例

    一次性定时器 div class dn div
  • C++,命名空间

    命名空间定义 namespace 命名空间名 变量名 函数 结构体 枚举名 全局引入命名空间 using namespace 命名空间名 部分引入命名空间 using namespace 命名空间名 变量名 注意 表示域限定符 在哪个位置使
  • 【实战加详解】二进制部署k8s高可用集群教程系列二 - ssl 证书简介

    TIP 二进制部署 k8s ssl 证书 转载请注明出处 https janrs com 1 ssl 证书简介 NOTE 在这里只做应用的简介 原理不做介绍 自行谷歌查阅 涉及到安全的 也不整理文档了 自行研究 1 1 什么是 ssl 证书
  • Java实现:寻找水仙花数

    Java实现 寻找水仙花数 文章目录 Java实现 寻找水仙花数 1 问题 2 解决方案 3 实现代码 4 执行结果 5 解决方法说明 穷举法 1 问题 寻找水仙花数 如果一个3位数等于其各位数字的立方和 则称这个数为水仙花数 例如 407
  • 奇异值分解方法求解最小二乘问题的原理

    文章目录 一 奇异值分解 SVD 原理 1 1 回顾特征值和特征向量 1 2 SVD的定义 1 3 求出SVD分解后的U V矩阵 1 4 SVD计算举例 1 5 SVD的一些性质 二 线性最小二乘问题 2 1 最小二乘问题复习 2 2 奇异
  • 最小二乘法拟合圆(Python)

    上文已经对比了三种数据点拟合圆的方法 本文分享最小二乘法的拟合过程 旨在了解如何用Python编程拟合圆 usr bin env python coding utf 8 This program is debugged by Harden
  • Markdown插入视频、mp3音频和gif图的语法

    总所周知 鄙人有一个专栏叫做 差生文具多 里面记录了一些工具的使用方法 以及使用工具时会遇到的一些问题 于是乎 之前就有一个朋友在群里问道 markdown如何插入视频 当时的我不会弄 然后就出现了下面这一幕 明天就弄 开整 然而这篇博客是
  • ARM64_VS2017 动态库 静态库编译 主程序调用环境搭建

    ARM64 VS2017 动态库 静态库编译 主程序调用环境搭建 1 打开Visual Studio Installer安装linux开发环境 2 配置linux环境 工具 gt 选项 gt 跨平台 gt 连接管理器 gt 添加 远程lin
  • mkdir()和mkdirs()区别

    mkdir 和mkdirs 区别如下 mkdirs 可以建立多级文件夹 mkdir 只会建立一级的文件夹 如下 new File tmp one two three mkdirs 执行后 会建立tmp one two three四级目录 n
  • 算法:回文字符串

    要求 给定一个字符串数组 判断出所有的回文字符串 class Solution public List
  • 预备打工人之SystemC学习 (五) 事务级建模库

    预备打工人之SystemC学习 TLM2 0基本概念 松散定时建模 近似定时模型 近似定时建模和松散定时建模的使用 发起者 目标 套接字和桥 DMI和调试传送结构 合并接口和套接字 名字空间和头文件 通用净核类 定义 构造 赋值和析构函数
  • JS栈内存和堆内存详解

    JS变量都存放在内存中 而内存给变量开辟了两块区域 分别为栈区域和堆区域 栈像个容器 容量小速度快 堆像个房间 容量较大 一 基本数据类型和引用数据类型存储方式 js中的数据类型可以分为基本类型和引用类型 基本类型是存在栈内存中的 引用类型
  • 在网页的JS中注入Hook

    在网页的JS中注入Hook Chrome浏览器的overrides的使用 itcoder cn 1 以下为Edge 的示例 1 本地新建一个目录 2 用浏览器关联该目录 选择目录后 浏览器上方会弹出一个横条提示确认 点击允许后即可关联 3
  • Java实现MD5加密及解密的代码实例分享

    原文地址 http www jb51 net article 86027 htm 如果对安全性的需求不是太高 MD5仍是使用非常方便和普及的加密方式 比如Java中自带的MessageDigest类就提供了支持 这里就为大家带来Java实现
  • muduo net库学习笔记4——事件驱动循环EventLoop、runInLoop和queueInLoop及对应唤醒

    首先总体情况 每个muduo网络库有一个事件驱动循环线程池 EventLoopThreadPool 线程池用在事件驱动循环上层 也就是事件驱动循环是线程池中的一个线程 每个TcpServer对应一个事件驱动循环线程池 每个线程池中有多个事件