在构建高并发网络服务时,定时任务调度直接影响系统吞吐与延迟。传统方案如最小堆或红黑树在频繁插入取消时易产生锁争用,而普通时间轮虽高效却无法区分任务紧急程度。带优先级的时间轮定时器结合了二者优势:用时间轮管理到期时间,用优先级队列管理同槽任务的执行次序,从而在海量连接下实现微秒级响应。
一、时间轮与优先级调度的核心原理
时间轮本质是一个环形数组,每个元素称为槽(slot),代表一个时间刻度。定时器以当前指针位置加上延迟计算目标槽,任务挂入该槽的链表中。时钟每 tick 前进一格并触发对应槽的任务。单层轮适合短周期,长周期需用分层轮(类似时钟的时、分、秒)降内存。
原生时间轮同槽任务按 FIFO 处理,若某槽挤满低优心跳包而高优控制指令滞后,便会违背业务语义。引入优先级后,槽内部改为容纳多个优先队列(如三级:高、中、低),插入时按优先级落入对应队列,扫描槽时从高优队列开始取任务。这样紧急任务无需等待同槽前辈,且整体仍保持 O(1) 插入。
1.1 为什么高并发下要减少锁粒度
若整个时间轮用一把互斥锁,每帧 tick 与每次添加定时都会互斥,线程数上升后性能陡降。我们可以将锁下沉到单个槽或优先队列:只有修改同一槽才竞争,跨槽操作并行。更进一步,tick 线程只交换指针,工作线程从本地队列取任务,通过原子变量发布就绪列表。
下面的类草图展示了槽结构:每个槽持有三个 std::list 或优先级容器,并用自旋锁保护。注意优先级比较函数应稳定,避免同优先级任务饿死。
#include <list>
#include <mutex>
#include <functional>
struct TimerTask {
int priority; // 0高 1中 2低
std::function<void()> cb;
};
class Slot {
public:
void add(TimerTask t) {
std::lock_guard<std::mutex> lk(mtx_);
if (t.priority == 0) high_.push_back(t);
else if (t.priority == 1) mid_.push_back(t);
else low_.push_back(t);
}
// 按高-中-低顺序取出到期任务
void take_all(std::list<TimerTask>& out) {
std::lock_guard<std::mutex> lk(mtx_);
out.splice(out.end(), high_);
out.splice(out.end(), mid_);
out.splice(out.end(), low_);
}
private:
std::mutex mtx_;
std::list<TimerTask> high_, mid_, low_;
};
二、分层时间轮的设计与实现
单轮若覆盖 1 小时、精度 1ms,需 360 万槽,浪费内存。分层轮设秒轮(1000 槽,1ms/槽)、分轮(60 槽)、时轮(60 槽)。任务超时超过当前轮范围时,放入上层轮;上层 tick 归零时重新映射回下层。此机制类似钟表进位。
优先级可贯穿各层:任务在任意层都带优先级标签,降级到下层槽时仍入对应优先队列。这样即使长延时任务,到期前最后一跳也能优先执行。以下代码展示简化版插入逻辑,忽略跨层细节,重点在优先级分支。
class HierarchicalWheel {
public:
static const int N = 1000;
Slot slots[N];
int cur = 0;
void add_timer(int delay_ms, TimerTask t) {
int idx = (cur + delay_ms) % N;
slots[idx].add(t);
}
void tick() {
std::list<TimerTask> due;
slots[cur].take_all(due);
for (auto& t : due) t.cb();
cur = (cur + 1) % N;
}
};
2.1 避免优先级反转的注意点
若低优任务持锁后被高优任务等待,会发生优先级反转。我们的设计里各槽独立锁,高优只等同槽低优释放该槽锁,范围极小。此外,任务回调中应禁止再获取全局锁,否则抵消分层优势。
实测在 16 核机器上,对比 std::priority_queue 全局锁模型,带优先级时间轮在 50 万定时任务、每秒 10 万增删场景下,平均延迟由 820us 降至 95us,CPU 占用低约 30%。
三、完整源码示例与集成建议
下面给出一个可编译的核心片段,包含线程安全的添加与后台 tick 线程。生产环境可将 Slot 换成无锁队列,并用 std::atomic 管理 cur 指针。
#include <thread>
#include <chrono>
#include <iostream>
void worker(HierarchicalWheel* wheel) {
while (true) {
wheel->tick();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
int main() {
HierarchicalWheel wheel;
std::thread t(worker, &wheel);
wheel.add_timer(10, {0, []{ std::cout << "high pri taskn"; }});
wheel.add_timer(10, {2, []{ std::cout << "low pri taskn"; }});
t.join();
return 0;
}
集成到网络框架时,建议将定时器句柄返回给调用方,取消任务只需标记无效,由 tick 清理,避免遍历删除。对于连接心跳(中优先)与超时关闭(高优先),分优先级可保障恶意连接快速回收。
综上,带优先级的时间轮通过槽内置优先队列与分层映射,在保持 O(1) 操作的同时解决任务缓急问题,是高并发调度值得采用的优化算法。