网络编程(17)——asio多线程模型IOThreadPool

十七、day17

之前我们介绍了IOServicePool的方式,一个IOServicePool开启n个线程和n个iocontext,每个线程内独立运行iocontext, 各个iocontext监听各自绑定的socket是否就绪,如果就绪就在各自线程里触发回调函数。为避免线程安全问题,我们将网络数据封装为逻辑包投递给逻辑系统,逻辑系统有一个单独线程处理,这样将网络IO和逻辑处理解耦合,极大的提高了服务器IO层面的吞吐率。

今天给大家介绍asio多线程模式的第二种多线程模式IOThreadPool,我们只初始化一个iocontext用来监听服务器的读写事件,包括新连接到来的监听也用这个iocontext。只是我们让iocontext.run在多个线程中调用,启动一个 io_context,由多个线程共享,这样回调函数就会被不同的线程触发,从这个角度看回调函数被并发调用了。

1. IOThreadPool实现

1) IOThreadPool.h

#pragma once
#include "Singleton.h"
#include <boost/asio.hpp>class AsioThreadPool : public Singleton<AsioThreadPool>
{
public:friend class Singleton<AsioThreadPool>;~AsioThreadPool() {}AsioThreadPool& operator = (const AsioThreadPool&) = delete;AsioThreadPool(const AsioThreadPool&) = delete;boost::asio::io_context& GetIOService();void Stop();
private:AsioThreadPool(int threadNum = std::thread::hardware_concurrency());boost::asio::io_context _service;std::unique_ptr<boost::asio::io_context::work> _work;std::vector<std::thread> _threads;
};
  • AsioThreadPool也是单例模式,需继承单例模板类Singleton<T>
  • _service:AsioThreadPool多线程模式中,只有一个io_context进行不同线程的调度,所以仅初始化一个ioc
  • _work:work的数量与ioc相同,有且仅有一个
  • _threads:线程数,默认与cpu核数相同

AsioThreadPool继承 Singleton 模板类时,Singleton 类的拷贝构造函数和赋值运算符已经被删除,这意味着AsioThreadPool也会继承这些删除操作。因此,派生类默认不能被拷贝或赋值,也不需要再次delte拷贝或者赋值,但是可以写上增加代码阅读性。

2)AsioThreadPool构造函数

AsioThreadPool::AsioThreadPool(int threadNum) : _work(new boost::asio::io_context::work(_service)){for (int i = 0; i < threadNum; ++i) {_threads.emplace_back([this]() {_service.run();});}
}

注意:work是通过std::unique_ptr进行管理的,所以work不能被拷贝,仅仅可以通过移动语义转移。

将AsioThreadPool池中唯一的ioc与work绑定,防止ioc返回,然后启动threadNum个线程,每个线程中都调用_service.run()。

_service.run函数内部就是从iocp或者epoll获取就绪描述符和绑定的回调函数,进而调用回调函数,因为回调函数是在不同的线程里调用的,所以会存在不同的线程调用同一个socket的回调函数的情况。

_service.run内部在Linux环境下调用的是epoll_wait返回所有就绪的描述符列表,在windows上会循环调用GetQueuedCompletionStatus函数返回就绪的描述符,二者原理类似,进而通过描述符找到对应的注册的回调函数,然后调用回调函数。

二者的流程如下:

a. IOCP

  • IOCP的使用主要分为以下几步:
    1 创建完成端口(iocp)对象
    2 创建一个或多个工作线程,在完成端口上执行并处理投递到完成端口上的I/O请求
    3 Socket关联iocp对象,在Socket上投递网络事件
    4 工作线程调用GetQueuedCompletionStatus函数获取完成通知封包,取得事件信息并进行处理。该函数会阻塞,直到有I/O操作完成并返回相应的事件信息,线程接收到通知后就可以处理对应的I/O请求。

b. epoll

  • 调用epoll_creat在内核中创建一张epoll表,这张 epoll 表用于管理要监视的文件描述符
  • 开辟一片包含n个epoll_event大小的连续空间,这个空间的大小是预期要监听的事件数量 n,以便存放准备好的事件信息。
  • 将要监听的socket(或其他文件描述符)注册到epoll表里,可以指定需要监听的事件类型(如可读、可写等),并将其与之前创建的 epoll 实例关联。
  • 调用epoll_wait,传入之前我们开辟的连续空间,这一调用会阻塞,直到有事件发生。一旦有事件就绪,epoll_wait 会返回就绪的 epoll_event 列表,并将对应的 Socket 信息写入之前开辟的内存空间中,供后续处理。

3)其他函数

boost::asio::io_context& AsioThreadPool::GetIOService() {return _service;
}void AsioThreadPool::Stop() {_work.reset();for (auto& t : _threads) {t.join(); }
}
  • GetIOService()用于返回多线程池中唯一的上下文服务
  • Stop()用于将work释放,以便ioc可以返回

2. 隐患及如何解决

IOThreadPool模式有一个隐患,同一个socket的就绪后,触发的回调函数可能在不同的线程里,比如第一次是在线程1,第二次是在线程3,如果这两次触发间隔时间不大,那么很可能出现不同线程并发访问数据的情况,比如在处理读事件时,第一次回调触发后我们从socket的接收缓冲区读数据出来,第二次回调触发,还是从socket的接收缓冲区读数据,就会造成两个线程同时从socket中读数据的情况,会造成数据混乱。因为多个线程都会执行io_context.run(),每次调用都会返回一些就绪的回调函数,比如,线程1调用io_context.run()返回ReadHandler1以及绑定的socket,线程2调用io_context.run()返回ReadHandler2以及绑定的socket,线程3调用io_context.run()返回ReadHandler3以及绑定的socket,但是ReadHandler1和ReadHandler3都是session1调用的,那么可能会导致多个线程同时处理相同的回调,导致线程安全问题

但如果回调函数被派发到逻辑队列中或者进行加锁,那么就不会存在不同线程访问同一个socket数据的线程安全问题。这里的隐患是不通过队列或者加锁,处理逻辑问题会存在线程安全问题。

1)通过strand改进

在多线程环境中触发回调函数时,我们可以使用 asio 提供的串行类 strand 来进行封装,从而实现串行调用。其基本原理是:每个线程在调用函数时,不直接执行该函数,而是将要调用的函数投递到由 strand 管理的队列中;随后,由一个统一的线程从队列中取出并调用这些回调函数。通过这种方式,函数调用是串行进行的,从而解决由于线程并发带来的安全问题。

使用strand进行封装

图中当socket就绪后并不是由多个线程调用每个socket注册的回调函数,而是将回调函数投递给strand管理的队列,再由strand统一调度派发。为了让回调函数被派发到strand的队列,我们只需要在注册回调函数时加一层strand的包装即可。

strand 实际上是将回调函数放入同一个队列(如果有逻辑层或者通过加锁处理回调,那么就不需要strand),以确保线程安全。因此,在描述符就绪后,相应的回调会被放入该队列中进行处理。虽然使用 strand 可以保证在多线程环境下的安全性,但在触发时仍然是单线程的,这限制了整体性能。因此,尽管多线程可以提高读写事件的派发效率,但整体性能通常不如IOService_pool 这种方式更优。在实际工作中,我们通常选择使用 IOService_pool,因为它能够更好地平衡并发和性能。

a. CSession类中新增成员变量_strand

boost::asio::strand<boost::asio::io_context::executor_type> _strand;

b. 修改CSession的构造函数

CSession::CSession(boost::asio::io_context& io_context, CServer* server):_socket(io_context), _server(server), _b_close(false),_b_head_parse(false), _strand(io_context.get_executor()){boost::uuids::uuid  a_uuid = boost::uuids::random_generator()();_uuid = boost::uuids::to_string(a_uuid);_recv_head_node = make_shared<MsgNode>(HEAD_TOTAL_LEN);
}

可以看到_strand的初始化是放在初始化列表里,利用io_context.get_executor()返回的执行器构造strand。因为在asio中无论iocontext还是strand,底层都是通过executor调度的,我们将他理解为调度器就可以。如果多个iocontext和strand的调度器是一个,那他们的消息派发统一由这个调度器执行。我们利用iocontext的调度器构造strand,这样他们统一由一个调度器管理。在绑定回调函数的调度器时,我们选择strand绑定即可。

比如我们在Start函数里添加绑定 ,将回调函数的调用者绑定为_strand

void CSession::Start(){::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),boost::asio::bind_executor(_strand, std::bind(&CSession::HandleRead, this,std::placeholders::_1, std::placeholders::_2, SharedSelf())));
}

修改前的代码为:

void CSession::Start() {memset(_data, 0, MAX_LENGTH); // 缓冲区清零// 从套接字中读取数据,并绑定回调函数headle_read_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2,shared_from_this()));//_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),//	boost::asio::bind_executor(_strand, //		std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2,shared_from_this())));
}

同样的道理,在所有收发的地方,都将调度器绑定为_strand, 比如发送部分我们需要修改为如下

    auto& msgnode = _send_que.front();boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len), boost::asio::bind_executor(_strand, std::bind(&CSession::HandleWrite, this, std::placeholders::_1, SharedSelf())));

3. 客户端

int main()
{try {auto pool = AsioThreadPool::GetInstance();boost::asio::io_context ioc;boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);// 必须异步等待,否则建立线程进行处理signals.async_wait([&ioc, pool](const boost::system::error_code& error, int signal_number) {if (!error) {std::cout << "Signal " << signal_number << " received." << std::endl;ioc.stop();  // 停止 io_contextpool->Stop();std::unique_lock<std::mutex> lock(mutex_quit);bstop = true;cond_quit.notify_one();}});CServer s(pool->GetIOService(), 10086);ioc.run();{std::unique_lock<std::mutex> lock(mutex_quit);while (!bstop) {cond_quit.wait(lock);}}}catch (std::exception& e) {std::cerr << "Exception: " << e.what() << '\n';}boost::asio::io_context io_context;
}

之所以用条件变量是因为传入Server的ioc是从线程池中获得的,Server初始化之后并没有调用ioc.run(),线程池中的ioc是运行在自己的线程中的,下面这段是运行在主线程中的,是为了防止主线程结束了而子线程还在运行,所以必须设置条件变量防止主线程结束

        {std::unique_lock<std::mutex> lock(mutex_quit);while (!bstop) {cond_quit.wait(lock);}}

如果不设置条件变量使主线程挂起,那么当主线程结束后,如果server还没有跑起来,server相当于子线程,那么会直接让子线程结束

CServer s(pool->GetIOService(), 10086);

单独生成一个ioc运行监听信号,当收到两个退出信号时,服务器退出,而server的ioc是从线程池中获取的。当获取到退出信号时,首先将监听信号的ioc退出,然后将线程池的unique_ptr释放work,使得线程池中的ioc能退出,最后上锁然后唤醒主线程,主线程结束。

                std::cout << "Signal " << signal_number << " received." << std::endl;ioc.stop();  // 停止 io_contextpool->Stop();std::unique_lock<std::mutex> lock(mutex_quit);bstop = true;cond_quit.notify_one();

4. 总结

1. 在IOServicePool示例中,使用main函数中定义的io_context来监听异常信号signal,并且用于初始化Server来监听接收信号;但在IOThreadPool示例中,main函数定义的ioc仅仅用于监听异常信号,而初始化Server启动异步接收async_accept是通过线程池中的ioc。

这是因为

第一种方式调用了n+1个ioc.run(),第二种方式调用了2个ioc.run()

  • 前者在main函数中处理信号监听和Server任务,当main中的ioc接收到客户端连接之后,生成一个子session,并从IOServicePool线程池中取一个ioc用于管理子Session的收发。也就是说,main中的ioc用于监听信号和server任务处理,而线程池中的ioc用于处理子Session的收发。main中的ioc一旦停止,那么服务器就会停止客户端的连接服务,session任务不会在增加,而线程池中的ioc全部停止后,线程的收发就会停止。
  • 后者再main函数中处理监听信号任务,而server任务处理交给了线程池IOThreadPool中唯一的ioc进行处理。也就是说,让一个ioc单独监听信号,另一个ioc运行在多线程中做收发。main中的ioc停止后,仅仅只是服务器的信号监听停止了,而server的任务处理以及session的收发仍然会持续。

此外,他们的退出机制也有些不同。

  • 第一种方式:信号处理函数在 ioc 上注册,当收到信号时,会调用 ioc.stop() 停止 io_context,并调用线程池的 Stop 方法停止线程池的运行。这种方式不需要额外的同步机制,因为主线程就是 ioc.run() 的执行线程,一旦停止,就会退出。
  • 第二种方式:信号处理函数在主线程的 ioc 上注册,但实际处理的 I/O 事件由线程池中的 io_context 完成。为了协调多线程退出,需要使用 mutex 和条件变量(cond_quit)来确保所有线程安全地退出。信号处理函数在停止 io_context 后,会通知等待在条件变量上的线程退出,从而确保整个程序能够正确结束。

2. 哪一种多线程模式的效率最高?

直观上来说可能二者的效率都差不多?其实IOServicePool的效率略高于IOThreadPool。

IOThreadPool 通常会涉及到 strandstrand 的作用是将回调函数放入同一个队列中执行,以确保线程安全(如果逻辑层或者通过加锁已经处理了回调的同步问题,那么就不需要使用 strand)。在这种机制下,当描述符就绪时,相应的回调会被放入这个队列中依次处理。虽然 strand 可以确保多线程环境下的安全性,但由于回调函数的执行是串行的,所以在事件触发时仍然是单线程操作,这在一定程度上限制了整体性能的提升。

尽管多线程模式确实能提高读写事件的派发效率,但由于 strand 的串行限制,整体性能通常不如使用 IOService_pool 的方式。在实际工作中,我们更倾向于使用 IOService_pool,因为它在保证线程安全的同时,能够更好地利用并发性,提升程序性能和效率。

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

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

相关文章

腾讯云SDK点播播放数据

点播播放质量监控提供点播播放全链路的数据统计、质量监控及可视化分析服务。支持实时数据上报、数据聚合、多维筛选和精细化定向分析&#xff0c;可帮助企业实时掌控大盘运营状况、了解用户习惯和行为特征&#xff0c;有效指导运营决策、驱动业务增长。 注意事项 点播播放质…

Python 工具库每日推荐 【Pandas】

文章目录 引言Python数据处理库的重要性今日推荐:Pandas工具库主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:销售数据分析案例分析高级特性数据合并和连接时间序列处理数据透视表扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 TypeScrip…

基于 CSS Grid 的简易拖拉拽 Vue3 组件,从代码到NPM发布(1)- 拖拉拽交互

基于特定的应用场景&#xff0c;需要在页面中以网格的方式&#xff0c;实现目标组件在网格中可以进行拖拉拽、修改大小等交互。本章开始分享如何一步步从代码设计&#xff0c;最后到如何在 NPM 上发布。 请大家动动小手&#xff0c;给我一个免费的 Star 吧~ 大家如果发现了 Bug…

探索未来:mosquitto-python,AI领域的新宠

文章目录 探索未来&#xff1a;mosquitto-python&#xff0c;AI领域的新宠背景&#xff1a;为何选择mosquitto-python&#xff1f;库简介&#xff1a;mosquitto-python是什么&#xff1f;安装指南&#xff1a;如何安装mosquitto-python&#xff1f;函数用法&#xff1a;5个简单…

代码随想录算法训练营第四十六天 | 647. 回文子串,516.最长回文子序列

四十六天打卡&#xff0c;今天用动态规划解决回文问题&#xff0c;回文问题需要用二维dp解决 647.回文子串 题目链接 解题思路 没做出来&#xff0c;布尔类型的dp[i][j]&#xff1a;表示区间范围[i,j] &#xff08;注意是左闭右闭&#xff09;的子串是否是回文子串&#xff0…

深入理解Transformer的笔记记录(精简版本)---- Transformer

自注意力机制开启大规模预训练时代 1 从机器翻译模型举例 1.1把编码器和解码器联合起来看待的话,则整个流程就是(如下图从左至右所示): 1.首先,从编码器输入的句子会先经过一个自注意力层(即self-attention),它会帮助编码器在对每个单词编码时关注输入句子中的的其他单…

【JavaEE】——回显服务器的实现

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;引入 1&#xff1a;基本概念 二&#xff1a;UDP socket API使用 1&#xff1a;socke…

2-118 基于matlab的六面体建模和掉落仿真

基于matlab的六面体建模和掉落仿真&#xff0c;将对象建模为刚体来模拟将立方体扔到地面上。同时考虑地面摩擦力、刚度和阻尼所施加的力&#xff0c;在三个维度上跟踪平移运动和旋转运动。程序已调通&#xff0c;可直接运行。 下载源程序请点链接&#xff1a;2-118 基于matla…

基于SpringBoot“花开富贵”花园管理系统【附源码】

效果如下&#xff1a; 系统注册页面 系统首页界面 植物信息详细页面 后台登录界面 管理员主界面 植物分类管理界面 植物信息管理界面 园艺记录管理界面 研究背景 随着城市化进程的加快和人们生活质量的提升&#xff0c;越来越多的人开始追求与自然和谐共生的生活方式&#xf…

使用激光跟踪仪提升码垛机器人精度

标题1.背景 码垛机器人是一种用于工业自动化的机器人&#xff0c;专门设计用来将物品按照一定的顺序和结构堆叠起来&#xff0c;通常用于仓库、物流中心和生产线上&#xff0c;它们可以自动执行重复的、高强度的搬运和堆垛任务。 图1 码垛机器人 传统调整码垛机器人的方法&a…

通信工程学习:什么是DIP数据集成点

DIP&#xff1a;数据集成点 DIP数据集成点&#xff08;Data Integration Point&#xff09;&#xff0c;简称DIP&#xff0c;是物联网技术&#xff08;IoT&#xff09;和机器到机器&#xff08;M2M&#xff09;通信中的一个重要组成部分。DIP在数据集成和传输过程中扮演着关键角…

【笔记】6.2 玻璃的成型

玻璃熔体的成型方法,有压制法(例如,制作水杯、烟灰缸等)、压延法(例如,制作压花玻璃等)、浇铸法(例如,制作光学玻璃、熔铸耐火材料、铸石等) 、吹制法(例如,制作瓶罐等空心玻璃)、拉制法(例如,制作窗用玻璃、玻璃管、玻璃纤维等)、离心法(例如,制作玻璃棉等)、喷吹法(例如,制作…

Ansible 工具从入门到使用

1. Ansible概述 Ansible是一个基于Python开发的配置管理和应用部署工具&#xff0c;现在也在自动化管理领域大放异彩。它融合了众多老牌运维工具的优点&#xff0c;Pubbet和Saltstack能实现的功能&#xff0c;Ansible基本上都可以实现。 Ansible能批量配置、部署、管理上千台主…

各类排序详解

前言 本篇博客将为大家介绍各类排序算法&#xff0c;大家知道&#xff0c;在我们生活中&#xff0c;排序其实是一件很重要的事&#xff0c;我们在网上购物&#xff0c;需要根据不同的需求进行排序&#xff0c;异或是我们在高考完报志愿时&#xff0c;需要看看院校的排名&#…

qt QGraphicsItem详解

一、概述 QGraphicsItem是Qt框架中图形视图框架&#xff08;Graphics View Framework&#xff09;的一个核心组件&#xff0c;它是用于表示2D图形元素的基类。 它支持的功能包括&#xff1a; 设置和获取图形项的位置和尺寸。控制图形项的外观&#xff0c;如颜色、笔刷、边框…

京东web 京东e卡绑定 第二部分分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 有相关问题请第一时间头像私信联系我删…

请求参数中字符串的+变成了空格

前端请求 后端接收到的结果 在URL中&#xff0c;某些字符&#xff08;包括空格、、&、? 等&#xff09;需要被编码。具体而言&#xff0c;在URL中&#xff0c;空格通常被编码为 或 %20。因此&#xff0c;如果你在请求参数中使用 &#xff0c;它会被解释为一个空格。 如果…

2024重生之回溯数据结构与算法系列学习(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】

欢迎各位彦祖与热巴畅游本人专栏与博客 你的三连是我最大的动力 以下图片仅代表专栏特色 [点击箭头指向的专栏名即可闪现] 专栏跑道一 ➡️ MYSQL REDIS Advance operation 专栏跑道二➡️ 24 Network Security -LJS ​ ​ ​ 专栏跑道三 ➡️HCIP&#xff1b;H3C-SE;CCIP——…

智能边缘计算 | 项目快速部署指南

在数字化浪潮的推动下&#xff0c;边缘计算与人工智能的深度融合正在成为推动智能社会发展的新动力。 边缘计算通过将数据处理和分析任务从中心服务器转移到更接近数据源的端侧&#xff0c;从而显著降低数据传输延迟&#xff0c;提高了响应速度和安全隐私性。在人工智能的加持…

python的特殊方法——魔术方法

前言 __init__(self[]) ​编辑 __call__(self [, ...]) __getitem__(self, key) __len__(self) __repr__(self) / __str__(self) __add__(self, other) __radd__(self, other) 参考文献 前言 官方定义好的&#xff0c;以两个下划线开头且以两个下划线结尾来命名的方法…