不用std::chrono直接sleep而用时间轮+最小堆,因sleep_until无法O(1)调整等待点、高频增删开销大;时间轮分桶归档,最小堆动态找最近到期时间;必须用steady_clock;堆中存shared_ptr避免拷贝;时间轮槽位需兼顾精度与内存,须记录轮数以防跨轮任务丢失。

为什么不用 std::chrono 直接 sleep 等待,而要搞时间轮 + 最小堆?
因为单线程里用 std::this_thread::sleep_until 做定时器,每次只能等最近一个任务,插入新任务或取消旧任务时无法 O(1) 调整等待点;高频率增删任务(比如每秒上千次)会导致频繁重设 sleep 时间,系统调用开销大、精度抖动明显。时间轮解决“批量到期检查”,最小堆解决“动态找最近到期时间”,两者互补:时间轮负责分桶归档,堆只维护各桶首节点的最早触发时刻。
std::chrono::steady_clock 是唯一可选时钟,别碰 system_clock
system_clock 可能被系统管理员手动调整,导致定时器提前/跳过执行;steady_clock 单调递增,才是真实流逝时间。所有时间计算必须基于它:
using Clock = std::chrono::steady_clock; using TimePoint = Clock::time_point; using Duration = Clock::duration;
常见错误是把 system_clock::now() 转成 steady_clock::time_point——没标准转换方式,强行 cast 会出错。所有时间点统一用 steady_clock 初始化、比较、相减。
最小堆只存指针,不存任务对象本身
堆里如果存 Task 值类型,每次 push/pop 都触发拷贝或移动构造,且任务可能带非 trivial 成员(如 std::function),性能差还易出错。正确做法是堆里存 std::shared_ptr 或裸指针(配合外部生命周期管理):
struct TaskCompare {
bool operator()(const std::shared_ptr<task>& a, const std::shared_ptr<task>& b) const {
return a->expire_time > b->expire_time; // 小顶堆
}
};
std::priority_queue<:shared_ptr>, std::vector<:shared_ptr>>, TaskCompare> heap;</:shared_ptr></:shared_ptr></task></task>
注意比较函数用 > 实现小顶堆(STL 默认大顶堆)。别漏掉 const 修饰符,否则编译不过。
时间轮槽位数不是越大越好,得匹配预期最大延迟
假设你希望支持最长 1 小时后触发的任务,精度要求 100ms,则时间轮总槽数至少为 3600s / 0.1s = 36000。但实际用 64 槽 + 多级轮(毫秒轮、秒轮、分钟轮)更省内存。单级轮简单场景下,推荐固定 256 或 1024 槽——太少导致冲突多(多个任务挤同一槽,遍历时链表变长),太多浪费内存且缓存不友好。关键逻辑是:
- 当前 tick 对应槽位:
index = (current_time / tick_duration) % num_slots - 插入任务时,算出它该落在哪个槽:
target_slot = (task_time / tick_duration) % num_slots - 但必须同时记录“第几轮后才触发”,否则跨轮任务会被漏掉
这个“轮数”字段不能丢,它是时间轮支持超长延时的核心。很多人只存槽索引,结果 1 小时后任务在第 0 槽反复被扫却永不执行。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











