std::chrono + std::thread 不适合高并发定时任务,因其 sleep_until 在大量任务下退化为线性扫描或频繁唤醒,而单任务单线程则资源开销大、调度抖动强、生命周期难统一管理。

为什么 std::chrono + std::thread 不适合高并发定时任务
因为 std::this_thread::sleep_until 在大量任务下会退化成线性扫描或频繁唤醒,每个任务单独起线程更不可取——资源开销大、调度抖动强、无法统一管理生命周期。时间轮(Timing Wheel)本质是空间换时间:用固定大小数组 + 滚动指针避免遍历全部待执行任务,O(1) 插入、均摊 O(1) 推进。
关键不是“轮子多大”,而是如何让 std::atomic<uint64_t></uint64_t> 刻度推进与任务插入/触发不互相阻塞。常见错误是所有操作都加一把大锁,结果吞吐卡死在锁竞争上。
- 单层时间轮只适合短周期(比如 0–60s),超时任务得降级到下一层,否则槽位爆炸
- 必须区分「插入时的绝对到期时间」和「轮内相对槽位索引」,转换错一帧就偏移 10ms+
- 不要用
std::vector<:list<task>></:list<task>>存桶——std::list分配碎片多,cache miss 高;改用std::vector<std::vector<Task>>或定制 slab 分配器
如何设计无锁插入 + 批量触发的 tick 推进逻辑
核心是把「时间推进」和「任务触发」解耦:一个生产者线程(tick 线程)只负责原子递增全局 current_tick,多个工作线程可并发调用 add_task(),通过 current_tick 计算目标槽位并 CAS 写入。触发阶段由单个 dispatcher 线程批量取出当前槽中所有任务,避免多线程争抢同一桶。
示例关键片段:
立即学习“C++免费学习笔记(深入)”;
// 槽索引 = (expire_tick - base_tick) & (bucket_count - 1) // 必须保证 bucket_count 是 2 的幂,才能用位与替代取模 uint32_t slot = (task.expire_tick - current_base_.load()) & (kBucketCount - 1); buckets_[slot].push_back(std::move(task));
-
current_base_每 kBucketCount 次 tick 向前跳一次,模拟“轮子滚动”;需用std::atomic<uint64_t>::fetch_add</uint64_t>配合 compare_exchange_strong 防止跳变撕裂 - 插入时不做任何 sleep 或 wait,失败就重试——这是无锁前提
- 触发任务前先
std::move整个std::vector<Task>桶,清空原容器,防止触发过程中被其他线程插入干扰
怎么处理跨层级任务迁移(比如从 500ms 轮迁到 5s 轮)
多级时间轮不是嵌套,而是并行维护多个独立轮(毫秒轮、秒轮、分钟轮),每层有不同粒度 tick_duration 和不同大小 bucket_count。任务初始插入最细粒度轮;当其到期时间超出该轮覆盖范围,就“升级”到上一层轮,并按上层粒度重新计算槽位。迁移动作本身必须原子——要么全成功,要么保留在原轮,不能丢失。
典型错误是迁移时直接 erase 原桶再 insert 新桶,中间存在窗口期导致任务漏触发。正确做法是:先写入目标轮对应桶,再用原子 flag 标记原任务为「已迁移」,最后由后台 sweep 线程清理旧桶中已标记项。
- 毫秒轮:64 槽 × 16ms → 覆盖 1024ms;超出则交由秒轮(60 槽 × 1s)
- 秒轮满后交由分钟轮(60 槽 × 60s),以此类推;最多 3 层足够覆盖小时级任务
- 迁移不发生在 tick 过程中,而是在
add_task()时预判——减少运行时分支判断
std::shared_ptr<Task> 还是裸指针 + 自定义内存池
用 std::shared_ptr 看似省事,但每次插入/触发都要原子引用计数,高并发下 cache line bouncing 明显。实测 QPS 超过 50k 后,refcount 开销占到整体 12%+。更优解是预分配 block 内存池(比如每块 4KB),用 std::atomic<size_t> 管理游标,分配/释放都是纯指针运算,零原子操作。
注意:内存池必须线程局部(thread_local static Pool<Task> pool;)或带分段锁,否则又回到锁瓶颈。任务回调函数里禁止再调用 add_task() ——这会引发重入,必须转成 post 到 event loop 或队列延后处理。
- 回调中若需再调度,应走
dispatcher_->post([this]{ add_task(...); });,而非直接调用 - 销毁任务对象前,确保其已从所有轮中移除(通过
cancel()标记 + 弱引用检查) - 别忘了对
std::function成员做 move 构造优化,避免隐式拷贝捕获对象
实际最难的部分不在轮子结构本身,而在 tick 线程与任务插入线程之间的时间差补偿——硬件时钟非均匀、调度延迟、CPU 频率缩放都会让 current_tick 和真实流逝时间产生 drift。必须定期用 clock_gettime(CLOCK_MONOTONIC) 校准 base offset,且校准过程不能停写入。



















