std::chrono+std::thread无法支撑百万级定时器,因线程开销大或轮询精度低;时间轮以空间换时间,o(1)插入与到期处理,需分层设计、无锁队列、原子环形缓冲区、句柄生命周期管理及cpu核绑定。

为什么 std::chrono + std::thread 无法扛住百万级定时器
因为每个定时器都用 std::thread 或甚至 std::async 拉起一个等待线程,内存和上下文切换开销会指数级增长;而用 std::this_thread::sleep_until 轮询单线程,又无法做到亚毫秒精度且 CPU 占用飙升。真正瓶颈不在“怎么触发”,而在“怎么组织海量到期事件”。
时间轮(Timing Wheel)本质是空间换时间:把未来一段时间切分成固定槽(slot),每个槽挂一个任务链表,指针匀速拨动,只检查当前槽的任务——O(1) 插入、均摊 O(1) 到期处理,内存占用与活跃定时器数无关,只与时间跨度和精度有关。
实操建议:
- 避免用
std::map<time_point task></time_point>或std::priority_queue做调度核心,它们插入/删除都是 O(log n),n=100w 时单次操作已超 20μs,无法满足高频调度 - 一级时间轮适合 0–60s、精度 10ms 的场景;若需支持 24h 后触发,必须分层(如:毫秒轮 + 秒轮 + 分轮),否则数组过大或溢出
- 务必用无锁队列(如
moodycamel::ConcurrentQueue)承接从时间轮中摘出的待执行任务,防止 tick 线程被业务回调阻塞
如何用 std::atomic + ring buffer 实现无锁 tick 推进
时间轮的“指针”不能用普通变量加锁更新——高并发下 ++cur_slot 会成为热点。正确做法是让 tick 线程独占推进,用 std::atomic_uint32_t 存当前槽索引,所有添加定时器的线程只读不写。
关键点在于环形缓冲区(ring buffer)的边界判断:槽总数为 N,当前 tick 是 tick_idx,则一个定时器在 delay_ms = 500 后触发,应放入槽位 (tick_idx + delay_ms / resolution_ms) % N。注意整数除法截断误差,实际要向上取整。
示例(精度 10ms,总长 6000 槽 → 覆盖 60s):
uint32_t resolution_ms = 10; uint32_t wheel_size = 6000; uint32_t target_slot = (cur_tick.load() + (delay_ms + resolution_ms - 1) / resolution_ms) % wheel_size;
常见错误:
- 直接用
delay_ms / resolution_ms导致 13ms 定时器被算成 1 槽(即 10ms),提前到期 - 未对
target_slot做% wheel_size,delay 过大时数组越界 - 在 tick 线程里直接调用用户回调——一旦某个回调卡住 100ms,整个轮子就停摆
定时器对象生命周期怎么不出 dangling pointer
C++ 里最危险的不是性能,是 shared_ptr<timer></timer> 被析构后,时间轮槽里还存着野指针。不能依赖引用计数自动清理:任务可能在轮子里挂了 10 分钟,而创建它的作用域早已退出。
正确做法是分离“注册”和“持有”:用户拿到的是轻量句柄(如 uint64_t timer_id),底层用 std::vector<:unique_ptr>></:unique_ptr> 管理真实对象,id 是 vector 下标 + 版本号(防复用冲突)。取消定时器时,仅原子标记该 slot 中对应节点为 cancelled,tick 线程摘链时跳过即可。
实操要点:
- 不要在
Timer析构函数里尝试从时间轮中移除自己——此时轮子可能正在遍历该槽 - 用
std::atomic_flag替代 bool 成员做 cancel 标记,确保多线程读写安全 - 如果支持重复定时器(repeating),必须在每次触发后重新计算下一次槽位并插入,不能原地修改——插入过程非原子,可能被其他线程观察到中间态
Linux 上怎么把 tick 线程绑定到隔离 CPU 核并禁用频率调节
即使算法最优,若 tick 线程被调度器频繁迁移或 CPU 频率动态降频,10ms 精度也会漂移到 15ms 以上。这不是代码问题,是 OS 层面干扰。
必须做三件事:
- 启动时用
sched_setaffinity将 tick 线程绑死到某个 CPU 核(如 core 3),并设为SCHED_FIFO实时策略 - 关闭该核的 CPUFreq:写
/sys/devices/system/cpu/cpu3/cpufreq/scaling_governor为performance - echo 1 > /proc/sys/kernel/preempt
否则,你看到的 “平均延迟 8.2ms” 很可能是统计均值,而 P99 延迟卡在 47ms——因为某次 tick 正好撞上内核软中断或页回收。
这步常被忽略:算法再漂亮,跑在不可控的调度环境里,就是纸上谈兵。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











