当前位置: 首页 > news >正文

C++ 网络编程(13) asio多线程模型IOServicePool

🎯 C++ Boost.Asio 多线程模型快速上手指南:从单线程到多 io_context
📅 更新时间:2025年7月1日
🏷️ 标签:C++ | Boost.Asio | 多线程 | 网络编程 | io_context

文章目录

  • 前言
  • 一、单线程和多线程对比
    • 单线程模式图
    • IOServicePool类型的多线程模型
    • IOServicePool多线程模式特点
  • 二、IOServicePool实现
    • 1.IOServicePool的声明
    • 2.IOServicePool的声明详解
      • 1.详解一
      • 2.详解二
      • 3.详解三
      • 4.详解四
    • 3.IOServicePool函数实现
    • 3.IOServicePool函数实现详解
      • 1.详解一 构造函数
      • 详解二 GetIOService()
      • 详解三 Stop()
  • 三、服务器入口实现优雅退出
  • 四、测试与总结


前言

前面的设计,我们对asio的使用都是单线程模式,为了提升网络io并发处理的效率,这一次我们设计多线程模式下asio的使用方式总体来说asio有两个多线程模型

第一个是启动多个线程,每个线程管理一个iocontext

第二种是只启动一个iocontext,被多个线程共享

后面的文章会对比两个模式的区别,这里先介绍第一种模式,多个线程,每个线程管理独立的iocontext服务


提示:以下是本篇文章正文内容,下面案例可供参考

一、单线程和多线程对比

单线程模式图

在这里插入图片描述

IOServicePool类型的多线程模型

在这里插入图片描述

IOServicePool多线程模式特点

  1. 每一个io_context跑在不同的线程里,所以同一个socket会被注册在同一个io_context里,它的回调函数也会被单独的一个线程回调,那么对于同一个socket,他的回调函数每次触发都是在同一个线程里就不会有线程安全问题,网络io层面上的并发是线程安全的。

  2. 但是对于不同的socket,回调函数的触发可能是同一个线程(两个socket被分配到同一个io_context),也可能不是同一个线程(两个socket被分配到不同的io_context里)。所以如果两个socket对应的上层逻辑处理,如果有交互或者访问共享区,会存在线程安全问题。比如socket1代表玩家1,socket2代表玩家2,玩家1和玩家2在逻辑层存在交互,比如两个玩家都在做工会任务,他们属于同一个工会,工会积分的增加就是共享区的数据,需要保证线程安全。可以通过加锁或者逻辑队列的方式解决安全问题,目前采取了后者

  3. 多线程相比单线程,极大的提高了并发能力,因为单线程仅有一个io_context服务用来监听读写事件,就绪后回调函数在一个线程里串行调用, 如果一个回调函数的调用时间较长肯定会影响后续的函数调用,毕竟是穿行调用。而采用多线程方式,可以在一定程度上减少前一个逻辑调用影响下一个调用的情况,比如两个socket被部署到不同的iocontext上,但是当两个socket部署到同一个iocontext上时仍然存在调用时间影响的问题不过通过逻辑队列的方式将网络线程和逻辑线程解耦合了,不会出现前一个调用时间影响下一个回调触发的问题

二、IOServicePool实现

IOServicePool本质上是一个线程池,基本功能就是根据构造函数传入的数量创建n个线程和iocontext,然后每个线程跑一个iocontext,这样就可以并发处理不同iocontext读写事件了

1.IOServicePool的声明

#pragma once
#include"Singleton.h"
#include<boost/asio.hpp>
#include<vector>class AsioIOServicePool:public Singleton<AsioIOServicePool>
{friend Singleton<AsioIOServicePool>;
public:using IOService = boost::asio::io_context;using WorkGuard = boost::asio::executor_work_guard<boost::asio::io_context::executor_type>;using WorkGuardPtr = std::unique_ptr<WorkGuard>;~AsioIOServicePool();AsioIOServicePool(const AsioIOServicePool& a) = delete;AsioIOServicePool& operator= (const AsioIOServicePool& a) = delete;boost::asio::io_context& GetIOService();void Stop();private:AsioIOServicePool(std::size_t size = std::thread::hardware_concurrency());std::vector<IOService> _ioServices;//将所有的上下文放入容器中std::vector<WorkGuardPtr> _workguards;//一个work对应一个上下文  防止提前退出std::vector<std::thread> _threads;//每个线程std::size_t _nextIOServices;//轮到哪一个上下文了};

2.IOServicePool的声明详解

1.详解一

我们用的是单例模式,所以我们一开始先继承我们之前写好的单例模式的模板

class AsioIOServicePool:public Singleton<AsioIOServicePool>

为了方便简写,我们将上下文 io_contextexecutor_work_guard 进行命名

using IOService = boost::asio::io_context;
using WorkGuard = 
boost::asio::executor_work_guard<boost::asio::io_context::executor_type>;

2.详解二

这里我们来介绍一下这个 executor_work_guard

可以保持 io_context::run() 不会提前返回,即使没有任务
当你想让 io_context 退出时,只需销毁 work_guard 对象(或调用 work_guard.reset()

我们的思路是,一个上下文 io_context 搭配一个 excutor_work_guard 这样可以防止每个线程中的每个上下文不会因为暂时没有任务而提前退出

3.详解三

这里我们将 excutor_work_guard 放入智能指针unique_ptr

using WorkGuardPtr = std::unique_ptr<WorkGuard>;

是为了这个 WorkGuard 不被拷贝,只能移动,并且从头用到尾,当结束的时候调用
.reset() 即可

4.详解四

boost::asio::io_context& GetIOService();

这个函数是返回一个上下文 io_context ,当一个客户端来的时候,我们要将这个客户了派发到我们一开始创建的多个线程中的一个线程中,用这个上下文去接收,用它来管理该客户端的 socket 和异步操作

3.IOServicePool函数实现

#include "AsioIOServicePool.h"AsioIOServicePool::AsioIOServicePool(std::size_t size) :
_ioServices(size), _workguards(size), _nextIOServices(0)
{for (int i = 0; i < size; i++)//给每一个work绑定一个上下文{_workguards[i] =std::unique_ptr<WorkGuard>(new WorkGuard(_ioServices[i].get_executor()));}//遍历每个上下文 创建多个线程  每个线程内部启动上下文for (int i = 0; i < size; i++){_threads.emplace_back([&]() {_ioServices[i].run();});}
}AsioIOServicePool::~AsioIOServicePool()
{std::cout << "AsioIOServicePool destruct" << std::endl;
}boost::asio::io_context& AsioIOServicePool::GetIOService()
{auto& service = _ioServices[_nextIOServices++];if (_nextIOServices >= _ioServices.size()){_nextIOServices = 0;}return service;
}void AsioIOServicePool::Stop()
{for (auto& work : _workguards){work.reset();//将每一个work绑定的上下文接触}for (auto& t : _threads)//等待所有的上下文停止后再退出,//io_context退出也需要花费时间{t.join();}
}

3.IOServicePool函数实现详解

1.详解一 构造函数

AsioIOServicePool::AsioIOServicePool(std::size_t size) 
:_ioServices(size), _workguards(size), _nextIOServices(0)
{for (int i = 0; i < size; i++)//给每一个work绑定一个上下文{_workguards[i] = std::unique_ptr<WorkGuard>(new WorkGuard(_ioServices[i].get_executor()));}//遍历每个上下文 创建多个线程  每个线程内部启动上下文for (int i = 0; i < size; i++){_threads.emplace_back([this,i]() {_ioServices[i].run();});}
}

我们在构造函数中首先要传入一个参数,根据这个参数我们来明确要创建多少个上下文io_context ,多少个线程 多少个WorkGuard
然后我们给每个WorkGuard依次绑定一个上下文io_context
随后创建线程并且开启上下文

详解二 GetIOService()

boost::asio::io_context& AsioIOServicePool::GetIOService()
{auto& service = _ioServices[_nextIOServices++];if (_nextIOServices >= _ioServices.size()){_nextIOServices = 0;}return service;
}

这里是当我们有客户端连接时,我们要返回一个上下文io_context 去连接管理与这个客户的会话,

详解三 Stop()

void AsioIOServicePool::Stop()
{for (auto& work : _workguards){work.reset();//将每一个work绑定的上下文接触}for (auto& t : _threads)//等待所有的上下文停止后再退出,//io_context退出也需要花费时间{t.join();}
}

WorkGuard.reset()
这一步销毁了每个WorkGuard,相当于告诉对应的 io_context:“你可以在没有任务时退出了”。
之前有 WorkGuard 保活,io_context::run() 不会返回;现在 reset 后,io_context::run() 会在任务处理完后自动退出

t.join()
这一步等待所有线程结束。
每个线程都在跑 _ioServices[i].run(),只有当对应的 io_context 退出(即 run() 返回)时,线程才会结束。
由于 io_context 退出可能需要一点时间(比如还有未完成的异步任务),所以 join 可能会阻塞一会儿,直到所有线程都安全退出

三、服务器入口实现优雅退出

int main()
{try {auto pool = AsioIOServicePool::GetInstance();boost::asio::io_context  io_context;boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);signals.async_wait([&io_context,pool](auto, auto) {io_context.stop();pool->Stop();});CServer s(io_context, 10086);io_context.run();}catch (std::exception& e) {std::cerr << "Exception: " << e.what() << endl;}
}

首先这部分是服务器的入口
下面我们进行详解

auto pool = AsioIOServicePool::GetInstance();

这里我们创建了一个实例,我们是通过GetInstance()函数来创建的,这个函数在上一篇文章中讲过,而且我们用的是单例模式,通过此函数,只能创建一个实例

 boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);signals.async_wait([&ioc](auto, auto){ioc.stop();});

这里我们创建一个信号集对象 signals 用于异步监听操作系统的信号
SIGINT:通常是 Ctrl+C 产生的中断信号。
SIGTERM:终止信号,常用于 kill 命令。
效果:当进程收到 SIGINTSIGTERM 时,signals 会捕获到
当捕获到的时候触发这个异步等待的函数

signals.async_wait([&ioc](auto, auto){ioc.stop();});

实现停止
这样你的服务就能优雅地停止,而不是被强制杀死

四、测试与总结

我们在客户端创建100个线程,每个线程进行500收发信息,我们来测试现在服务器的多线程效果如何

在这里插入图片描述

最终我们可以发现 总耗时大概100秒
在这里插入图片描述

今天总结了如何使用IOServicePool模式构造多线程模型的asio服务器

我们使用的方式是创建多个线程,并且给每一个线程都分配一个上下文io_context

http://www.lqws.cn/news/593209.html

相关文章:

  • CAU数据挖掘实验 表分析数据插件
  • 零信任安全管理系统介绍
  • 安防监控视频汇聚平台EasyCVR v3.7.2版云端录像无法在web端播放的原因排查和解决方法
  • 笔记本电脑怎样投屏到客厅的大电视?怎样避免将电脑全部画面都投出去?
  • Rust 是什么
  • [C#] WPF - 自定义样式(Slider篇)
  • WPF学习(三)
  • 08跨域
  • vue-i18n+vscode+vue 多语言使用
  • Mac 部署Latex OCR并优化体验(打包成App并支持全局快捷键)
  • WebSocket技术全面解析:从历史到实践
  • (Python)Python基础语法介绍(二)(Python基础教学)
  • 老年护理实训室建设方案:打造沉浸式护理实训环境
  • pulseaudio实现音频的网络传输
  • Wireshark TS | 诡异的光猫网络问题
  • 中心效应:多中心临床试验的关键考量
  • Selector组件组件
  • sentinel滑动窗口及熔断限流实现原理
  • STM32作为主机识别鼠标键盘
  • Gradio全解13——MCP协议详解(5)——Python包命令:uv与uvx实战
  • Easy Window UI设计器 - 图表组件 10秒完成UI效果
  • Xposed框架深度解析:Android系统级Hook实战指南
  • Flask+LayUI开发手记(十):构建统一的选项集合服务
  • QGIS合并、拆分SHP文件
  • 深入理解栈的合法弹出序列验证算法
  • docusaurus初步体验
  • Bootstrap 安装使用教程
  • 多bin技术:为LoRa项目赋能的高效远程升级方案
  • OpenCV CUDA模块设备层-----双曲正切函数tanh()
  • Terraform Helm:微服务基础设施即代码