Ray 异步基础设施(三):线程池 `IOServicePool` 与定时器 `PeriodicalRunner`
Ray 异步基础设施系列第三篇。← 上一篇:可观测性与混沌测试 · 下一篇:gRPC 与 Asio 的协作模式 →
1. IOServicePool:事件循环线程池
架构
┌─────────────────────────────────────────────┐
│ IOServicePool │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │io_ctx[0] │ │io_ctx[1] │ │io_ctx[N] │ │
│ │ Thread 0 │ │ Thread 1 │ │ Thread N │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
每个 io_context 拥有独立的线程,线程间完全隔离,业务逻辑在各自线程内串行执行,无需加锁。
初始化与运行
IOServicePool pool(4); // 创建 4 个 io_context,各自独立线程
pool.Run(); // 启动所有线程(内部使用 work_guard 保持运行)
获取 io_context 的两种策略
| 方法 | 策略 | 适用场景 |
|---|---|---|
Get() | 轮询(Round-Robin) | 无状态短任务,均匀分散负载 |
Get(hash) | 哈希固定 | 需要线程亲和性的对象(如同一连接的所有回调必须在同一线程上串行执行) |
哈希固定策略是关键:同一连接的所有请求映射到同一个 io_context,保证它们按序串行执行,无需额外同步。
2. PeriodicalRunner:周期性任务调度器
使用方式
auto runner = PeriodicalRunner::Create(io_context);
runner->RunFnPeriodically([] { SendHeartbeat(); }, 1000, "Heartbeat");
在指定的 io_context 上,每隔 1000ms 执行一次 SendHeartbeat。
生命周期安全设计
PeriodicalRunner 继承 std::enable_shared_from_this<PeriodicalRunner>,在定时器回调中捕获 weak_from_this():
timer.async_wait([weak_self = weak_from_this(), fn, period, timer](const auto& error) {
if (auto self = weak_self.lock()) {
if (error == boost::asio::error::operation_aborted) return;
fn();
self->DoRunFnPeriodically(fn, period, timer);
}
// weak_self.lock() 失败 → 对象已析构 → 静默退出
});
当外部释放 shared_ptr<PeriodicalRunner> 后,下一次定时器触发时 lock() 返回 nullptr,周期循环自动终止,无崩溃、无泄漏、无需显式取消。
这正是《C++ 异步编程:用
weak_from_this安全捕获对象生命周期》中介绍的模式在生产代码中的应用。
仪表化执行
当全局配置 event_stats 开启时,PeriodicalRunner 会额外记录从定时器到期到回调实际执行之间的排队延迟,增强端到端可观测性。
3. 两者的协作关系
IOServicePool::Get(hash) → instrumented_io_context*
↓
PeriodicalRunner::Create(*io_context)
↓
runner->RunFnPeriodically(...)
IOServicePool提供多线程运行载体(解决并发与负载均衡)PeriodicalRunner是绑定在某一载体上的闹钟(解决周期调度与生命周期安全)
Ray 中几乎所有后台定时逻辑(心跳发送、监控上报、GC 触发)都由这两个组件驱动。
总结
IOServicePool提供无锁并发:多线程各自运行独立io_context,线程亲和性由哈希保证。PeriodicalRunner通过weak_from_this()实现了生命周期安全的周期调度:所有者析构后定时回调自动退出。- 两者组合,构成了 Ray 后台任务调度的基础设施。