如何基于Boost.Asio的io_service实现单资源请求限流避免调度饥饿?
基于
io_service实现可配置并发上限的公平资源调度方案 你目前想到的多strand轮询方案属于可行的糙快猛实现,存在几个明显短板:并发上限调整需要重建strand、大量strand带来额外调度开销、无法做跨资源的全局公平性兜底,长期维护成本不低。更优雅的实现是做一套轻量的配额调度层,不需要依赖多strand,就能实现任意单资源并发上限控制,逻辑和性能都更好。
核心设计思路
本质是给每个命名逻辑资源维护独立的待执行队列+运行中计数,全局做轻量的准入和续调度,保证任意时刻单个资源同时跑的任务数不超过配置的上限,从根源上避免单资源占满所有执行线程。
基础数据结构
整个调度层只需要两类结构,全线程安全:
- 全局调度表:用
std::unordered_map<std::string, ResourceCtx>存储所有资源的上下文,key为资源的逻辑标识,配一把全局互斥锁保护表和上下文的读写(高并发场景可以换成分片锁降低竞争) - 单资源上下文
ResourceCtx:- 待执行任务队列:
std::queue<std::function<void()>> pending - 当前运行中任务数:
size_t running = 0 - 该资源的最大并发配额:
size_t max_concurrent(支持全局默认值,也可给特定资源单独配置)
- 待执行任务队列:
调度流程
整个逻辑没有复杂依赖,只需要处理两个时机:
- 新任务提交时
收到绑定指定资源的任务后,先加锁找到对应资源的上下文(不存在就新建,初始化对应并发配额):- 如果当前
running < max_concurrent:直接将任务包装一层完成回调后post到io_service,同时将running计数加1 - 如果已经达到并发上限:将任务放入该资源的
pending队列排队等待
- 如果当前
- 任务执行完成后
每个任务执行完会自动触发续调度(这层逻辑直接包装在投递到io_service的外层闭包里,不需要业务方手动调用):- 加锁找到对应资源的上下文,将
running计数减1 - 检查
pending队列是否为空:如果不为空,取出队首任务,包装完成回调后post到io_service,running计数加1;如果为空且running已经归0,可以选择将该资源上下文从全局表中删除,回收内存
- 加锁找到对应资源的上下文,将
所有计数修改、队列操作都在持锁状态下完成,天然不存在竞态,不需要额外依赖strand做串行保护。
这个方案对比多strand轮询的优势
- 配置灵活:运行时可以直接修改任意资源的
max_concurrent值,调整并发上限不需要重建任何结构,下一次续调度就会自动生效 - 开销极低:不管单资源配多少并发上限,每个资源只需要维护一个轻量队列,没有多strand带来的asio内部调度开销
- 公平性可控:如果需要做跨资源的全局公平(比如避免多个高优资源占满所有io线程),只需要在续调度层加一层轮询逻辑:当io线程空闲时,轮询所有存在等待任务的资源,只要没到并发配额就取任务投递,完全不会出现单资源任务堵在全局队列前面的情况
- 扩展容易:可以很方便地加队列长度截断、任务超时丢弃、优先级调度等定制逻辑,这些在多strand方案里实现成本极高
可选优化点
- 如果资源标识是固定枚举而非动态字符串,可以把全局调度表换成数组,锁开销可以进一步降低
- 投递任务到
io_service的操作可以移到锁外执行:持锁阶段只做计数修改、任务出队,拿到待执行任务后立刻解锁,再执行post操作,能大幅减少锁持有时间 - 如果业务里部分任务本身需要串行执行,完全可以在投递时给对应任务套一层单独的strand,和本调度机制不冲突——调度层只控制单资源的总并发数,不限制任务内部的串行要求
内容的提问来源于stack exchange,提问作者ki92
相关产品推荐
相关产品推荐

