std::chrono+std::thread不适合万级定时任务,因每任务一线程导致内存与调度开销剧增;时间轮通过分桶共享tick、timerfd+epoll实现高效无锁调度,多级设计需正确降级,内存池替代std::function避免碎片。

为什么 std::chrono + std::thread 不适合万级定时任务
直接用 std::this_thread::sleep_until 启一堆线程,每个任务一个线程,内存和调度开销会指数级上涨。Linux 默认线程栈 8MB,10000 个任务就是 80GB 虚拟内存;更关键的是内核要频繁切换上下文,perf sched 一看就能发现大量 schedule_timeout 和 __switch_to 开销。
真正压垮系统的不是“时间精度”,而是“每任务一资源”的设计。时间轮的核心思路是:把时间切片(tick),让多个任务共享同一个 tick 触发点。
- 单 tick 只需一次系统调用(如
epoll_wait或timerfd_settime),不管该 tick 上挂了多少任务 - 插入/删除操作复杂度是 O(1),不随任务总数增长
- 内存占用固定:比如 64 槽 × 256ms 精度 × 指针大小 = 不到 1KB
如何用 timerfd + epoll 实现无锁时间轮主体
timerfd_create 是 Linux 提供的、可被 epoll 监听的定时器 fd,比 setitimer 更可控,也比轮询 clock_gettime 更省 CPU。关键在于:它只通知“tick 到了”,不负责执行——执行逻辑必须由你收进 epoll 循环里做,否则就退化成多线程 sleep。
示例骨架:
立即学习“C++免费学习笔记(深入)”;
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
struct itimerspec ts = {.it_value = {0, 250000000}, .it_interval = {0, 250000000}};
timerfd_settime(tfd, 0, &ts, nullptr);
<p>epoll_ctl(epoll_fd, EPOLL_CTL_ADD, tfd, &ev); // ev.events = EPOLLIN
// 收到事件后:read(tfd, &exp, sizeof(uint64_t)) 清空计数,再遍历当前 tick 对应的 bucket 执行回调</p>- 不要在
epoll_wait回调里直接调用用户函数——可能阻塞或抛异常,应投递到工作线程池 -
timerfd_settime的it_interval必须设为 tick 周期,不能设 0(否则只触发一次) - 务必用
TFD_NONBLOCK,否则read可能阻塞整个事件循环
多级时间轮怎么避免“倒挂”和精度漂移
单层时间轮只能覆盖有限时间范围(比如 64×256ms ≈ 16s)。要支持小时级任务,就得用多级(如 3 层:毫秒级、秒级、分钟级)。但常见错误是:当任务到期前被“降级”到下一层时,没重算余数,导致提前或延后触发。
正确做法是每一层只管自己的模数,且降级时用完整剩余时间重新分配:
- 假设当前时间戳为
now,任务到期时间为expire,差值delta = expire - now - 毫秒轮槽位 =
delta % 64,若delta >= 64,则交给秒轮处理 - 秒轮槽位 =
(delta / 64) % 64,不是delta % 64—— 这里最容易写错 - 所有层级共享同一套 tick 推进逻辑,靠
timerfd统一驱动,不各自维护时钟
std::function 持有 lambda 会导致内存碎片?用什么替代
高频创建/销毁 std::function(尤其捕获大对象的 lambda)会触发大量小内存分配,malloc 在高并发下锁竞争严重。实测在 50k task/s 场景下,malloc_stats 显示 70% 时间花在 arena_get2 上。
更稳的做法是预分配 + 内存池:
- 定义固定结构体:
struct TimerNode { uint64_t expire; void* ctx; void (*cb)(void*); TimerNode* next; }; - 用
std::vector<timernode></timernode>预分配 64K 节点,用 freelist 管理空闲链表 - 回调函数指针 + 上下文指针替代
std::function,零额外分配 - 如果必须用 C++17 以上,
std::pmr::polymorphic_allocator配合std::pmr::vector也可行,但注意std::function内部仍可能逃逸到堆
真正难的不是实现轮子,而是让每个 tick 的处理时间稳定在微秒级——一旦某次 bucket 遍历超时,后续所有 tick 都会堆积,最终整个调度延迟雪崩。所以务必在执行回调前记录起始时间,超 50μs 就打日志并截断。


















