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

🎯 C++ Boost.Asio 多线程模型快速上手指南:从单线程到多 io_context

📅 更新时间:2025年7月1日

🏷️ 标签:C++ | Boost.Asio | 多线程 | 网络编程 | io_context

文章目录


前言

前面的设计,我们对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的声明

cpp 复制代码
#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.详解一

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

cpp 复制代码
class AsioIOServicePool:public Singleton<AsioIOServicePool>

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

cpp 复制代码
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

cpp 复制代码
using WorkGuardPtr = std::unique_ptr<WorkGuard>;

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

4.详解四

cpp 复制代码
boost::asio::io_context& GetIOService();

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

3.IOServicePool函数实现

cpp 复制代码
#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.详解一 构造函数

cpp 复制代码
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()

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

	return service;
}

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

详解三 Stop()

cpp 复制代码
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 可能会阻塞一会儿,直到所有线程都安全退出

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

cpp 复制代码
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;
    }
}

首先这部分是服务器的入口

下面我们进行详解

cpp 复制代码
auto pool = AsioIOServicePool::GetInstance();

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

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

这里我们创建一个信号集对象 signals 用于异步监听操作系统的信号
SIGINT:通常是 Ctrl+C 产生的中断信号。
SIGTERM:终止信号,常用于 kill 命令。

效果:当进程收到 SIGINTSIGTERM 时,signals 会捕获到
当捕获到的时候触发这个异步等待的函数

cpp 复制代码
signals.async_wait([&ioc](auto, auto)
     {
         ioc.stop();
     });

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

四、测试与总结

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

最终我们可以发现 总耗时大概100秒

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

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