从零手写高性能 C++ TCP 服务器框架(八):时间轮定时器实现
本文介绍了高性能C++ TCP服务器中时间轮定时器的实现方案。针对海量连接场景下的定时任务管理,对比了三种方案:数组遍历(O(N)复杂度)、小根堆(O(logN)插入删除)和时间轮(O(1)操作)。重点阐述了时间轮的设计思想,通过环形数组和指针推进模拟时钟机制,使用shared_ptr和weak_ptr解决任务生命周期管理问题。具体实现包括TimerTask封装定时任务、TimerWheel核心逻
从零手写高性能 C++ TCP 服务器框架(八):时间轮定时器实现
一、前言
引言
在 TCP 服务器中,我们经常需要处理超时断连、心跳检测、延迟重发等场景。如果每个连接都创建系统定时器,性能损耗太大。
假设我们有上万个连接,每时每刻都要检查哪些连接超时了,你会怎么设计?
实现方案:
常见有三种解决方案:
方案一:数组遍历 —— 简单但低效
最朴素的想法:把所有的定时任务存在一个数组里,每次需要检查超时的时候,就遍历整个数组,找出所有超时的任务并执行。
伪代码:
vector<TimerTask> tasks;
for (auto &task : tasks) {
if (now >= task.expire_time) {
task.callback();
}
}
问题:复杂度 O(N),N 上万个连接时,每毫秒遍历一次 CPU 占用会非常高。而且很多任务并未超时,却每次都要参与比较。
方案二:小根堆 —— 按超时时间排序
用小根堆存储任务,堆顶总是最早超时的任务。每次检查时,只需要看堆顶是否超时,超时就弹出并执行,直到堆顶未超时。
伪代码:
priority_queue<TimerTask, vector<TimerTask>, greater<>> heap;
while (!heap.empty() && heap.top().expire_time <= now) {
auto task = heap.top(); heap.pop();
task.callback();
}
优点:插入和删除 O(logN),检查超时 O(1)(只处理已超时的任务)。
缺点:如果需要“刷新”一个任务(比如收到心跳包,延长超时时间),无法直接更新堆中元素的位置,通常需要先删除再插入,删除操作 O(N) 或依赖额外的索引。
方案三:时间轮 —— 哈希表 + 循环数组
时间轮是解决海量定时任务的高效结构。本节我们将实现一个轻量级、与事件循环集成的时间轮定时器。
时间轮的思想来源于钟表,如果我们定了⼀个3点钟的闹铃,则当时针走到3的时候,就代表时间到了。
同样的道理,如果我们定义了一个数组,并且有⼀个指针,指向数组起始位置,这个指针每秒钟向后走动⼀步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定⼀个3s后的任务,则只需要将任务添加到tick+3位置,则每秒中走一步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。

问题:
1. 如果一个事件要在1个小时之后才被释放,那么数组的长度是多少?
2. 如果同一个时刻有多个事件要被释放,数组该怎么表示?
问题1: 图片中 数组是以秒为单位的,一定不可能用60×60=3600个长度的数组进行表示,效率太低了,我们可以在来一个数组,这个数组的单位是分钟,长度为60,再来一个数组,这个数组的单位是小时即可。

问题2:解决方案:将时间轮的一维数组设置为二维数据,这样就可以在同一时刻支持多个定时任务。
时间轮的核心思想,借鉴了操作系统的 CPU 调度时钟机制。我们可以把每个定时任务看作一个“进入了定时睡眠的进程”,把 _tick 的推进看作“时钟中断”。这种“用空间槽位换时间,实现 O(1) 级插入与查找”的策略,是它在面对海量连接时依然能保持高性能的理论基础。
二、TimerTask
编写事件任务
在 Linux 中,我们学习到一种思想:先组织,在管理
这个事件任务 TimerTask就是组织,管理交给 TimerWheel 进行,在此处我们先把 TimerTask 先进行编写,这个较为简单;等到下面编写 TimerWheel 之前我们还要在进行普遍,在此处,我们先编写 TimerTask。
class TimerTask
{
private:
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
public:
TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb) : _id(id), _timeout(delay), _task_cb(cb), _cancel(false) {}
// 在释放的时候,执行 _task_cb 任务
~TimerTask()
{
if (_cancel == false)
_task_cb();
_release_cb();
}
void SetReleaseFunc(ReleaseFunc &cb)
{
_release_cb = cb;
}
uint32_t DelayTime()
{
return _timeout;
}
void Cancel()
{
_cancel = true;
}
private:
uint64_t _id; // 唯一id
uint32_t _timeout; // 超时时间
bool _cancel; // 是否删除
TaskFunc _task_cb; // 定时器需要执行的任务
ReleaseFunc _release_cb; // 删除TimerWheel中定时器的信息
};
三、TimerWheel
我们先根据上面的讲述,我写一下 TimerWheel 的相关成员,然后看看该如何进行改进:
class TimerWheel
{
public:
private:
int _tick;
int _capacity;
std::vector<std::vector<TimerTask>> _wheel;
std::unordered_map<uint64_t, TimerTask> _timers;
};
相关成员
1. _tick
作用:当前秒针(或时间轮刻度)的位置,取值范围 [0, _capacity-1]。
每经过一个时间单位(例如 1 秒),_tick 就会前进一格。当 _tick 指向某个槽位时,该槽位中的所有 TimerTask 都应该被“执行超时”(即清理并触发回调)。
2. _capacity
作用:时间轮的槽位总数,决定了定时器能支持的最大延时。
例如 _capacity = 60,那么最大延时为 60 秒。这也是环形数组 _wheel 的大小。
3. _wheel
类型:std::vector<std::vector<TimerTask>>
作用:时间轮的核心环形数组。外层 vector 的长度为 _capacity,每个元素是一个 vector<TimerTask>,代表该槽位上存放的所有定时任务。
添加任务时,根据 (当前_tick + 延时) % _capacity 计算出目标槽位,将 TimerTask 对象拷贝或移动进对应的内层 vector。
4. _timers
类型:std::unordered_map<uint64_t, TimerTask>
作用:这是一个用于快速查找的辅助映射表。键是任务的唯一 ID,值是一个 TimerTask。
问题:
上面的大体逻辑没有错,但是有问题:TimerTask 的生命周期管理!
1. TimerTask的 _task_cb 回调函数是在TimerTask析构函数中执行的,那么该在哪里对TimerTask进行释放呢?如果是主动释放,是否会造成野指针呢?
2. 一个具体的场景:
一个连接如果在 30 秒内没有任何通信,就需要被销毁(非活跃连接)。
若在连接建立时直接添加一个 30 秒后销毁的定时任务,那么即使该连接在 30 秒内有数据通信,这个定时任务依然会在 30 秒后触发,导致活跃连接被错误销毁。 
解决方案:类的析构函数 + 智能指针shared_ptr,通过这两个技术可以实现定时任务的延时
1. 使用一个类,对定时任务进行封装,类实例化的每一个对象,就是一个定时任务对象,
当对象被销毁的时候,再去执行定时任务(将定时任务的执行,放到析构函数中)
2. shared_ptr用于对new的对象进行空间管理,当shared_ptr对一个对象进行管理的时候,内部有一个计数器,计数器为0的时候,则释放所管理的对象。

std::shared_ptr<int> pi(a); --- a对象只有在pi计数为0的时候,才会被释放
std::shared_ptr<int> pi1(pi) -- 当针对pi又构建了一个shared_ptr对象,则pi和pi1计数器为2当pi和pi1中任意一个被释放的时候,只是计数器-1,因此他们管理的a对象并没有被释放,
只有当pi和pi1都被释放了,计数器为0了,这时候才会释放管理的a对象
那么此时的 _wheels 的类型就变为
using PtrTask = std::shared_ptr<TimerTask>;
std::vector<std::vector<PtrTask>> _wheel;
还有问题吗?有的,_timers 的 TimerTask 类型也要改成 PtrTask 吗?答案:不是
我们首先要明确 _wheel 和 _timers 的作用是什么?
_wheel 持有 任务的实际所有权,而 _timers 只提供快速查找能力,但不参与所有权的管理。
- shared_ptr:_wheel 持有强引用 → 需要显式删除 _timers 中的条目才能释放对象。
- weak_ptr:不增加引用计数,不影响对象的生命周期。对象的存活完全由 _wheel 中的 shared_ptr 决定。
因此要把 _timers 的 TimerTask 类型也要改成 WeakPtr。
最终的相关成员:
using PtrTask = std::shared_ptr<TimerTask>;
using WeakTask = std::weak_ptr<TimerTask>;
int _tick; // 指针指向的位置
int _capacity; // 数组的容量,也是支持任务的最大延迟时间
std::vector<std::vector<PtrTask>> _wheel; // 时间轮
std::unordered_map<uint64_t, WeakTask> _timers;
相关接口:
1. Timer —— 添加定时任务
3. TimerRefresh —— 刷新定时任务(延迟超时)
4. TimerCancel—— 取消定时任务
5. RunTimerTask —— 驱动时间轮前进
6. 私有辅助方法 RemoveTimer
代码编写:
class TimerWheel
{
private:
void RemoveTimer(uint64_t id)
{
auto it = _timers.find(id);
if (it != _timers.end())
{
_timers.erase(it);
}
}
public:
TimerWheel() : _tick(0), _capacity(60), _wheel(_capacity) {}
// 添加定时任务
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
PtrTask pt(new TimerTask(id, delay, cb));
// 将释放函数放到ReleaseFunc中
pt->SetReleaseFunc(std::bind(&TimerWheel::RemoveTimer, this, id));
// 放入到时间轮和_timers
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
_timers[id] = WeakTask(pt);
}
void TimerRefresh(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
return;
PtrTask oldTask = it->second.lock();
if (!oldTask)
{
_timers.erase(it);
return;
}
uint32_t delay = oldTask->DelayTime();
TaskFunc cb = oldTask->GetCallback();
_timers.erase(it); // 先删除旧映射
oldTask->Cancel(); // 取消旧任务
TimerAdd(id, delay, cb); // 添加新任务
}
void TimerCancel(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
{
return;
}
PtrTask pt = it->second.lock();
if (pt)
pt->Cancel();
}
void RunTimerTask()
{
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear();
}
private:
using PtrTask = std::shared_ptr<TimerTask>;
using WeakTask = std::weak_ptr<TimerTask>;
int _tick; // 指针指向的位置
int _capacity; // 数组的容量,也是支持任务的最大延迟时间
std::vector<std::vector<PtrTask>> _wheel; // 时间轮
std::unordered_map<uint64_t, WeakTask> _timers;
};
四、测试代码
#include <iostream>
#include <time.h>
#include <cstdint>
#include <functional>
#include <unordered_map>
#include <vector>
#include <unistd.h>
#include <memory>
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
class TimerTask
{
private:
public:
TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb) : _id(id), _timeout(delay), _task_cb(cb), _cancel(false) {}
// 在释放的时候,执行 _task_cb 任务
~TimerTask()
{
if (_cancel == false)
_task_cb();
_release_cb();
}
TaskFunc GetCallback() const { return _task_cb; }
void SetReleaseFunc(const ReleaseFunc &cb)
{
_release_cb = cb;
}
uint32_t DelayTime()
{
return _timeout;
}
void Cancel()
{
_cancel = true;
}
private:
uint64_t _id; // 唯一id
uint32_t _timeout; // 超时时间
bool _cancel; // 是否删除
TaskFunc _task_cb; // 定时器需要执行的任务
ReleaseFunc _release_cb; // 删除TimerWheel中定时器的信息
};
class TimerWheel
{
private:
void RemoveTimer(uint64_t id)
{
auto it = _timers.find(id);
if (it != _timers.end())
{
_timers.erase(it);
}
}
public:
TimerWheel() : _tick(0), _capacity(60), _wheel(_capacity) {}
// 添加定时任务
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
PtrTask pt(new TimerTask(id, delay, cb));
// 将释放函数放到ReleaseFunc中
pt->SetReleaseFunc(std::bind(&TimerWheel::RemoveTimer, this, id));
// 放入到时间轮和_timers
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
_timers[id] = WeakTask(pt);
}
void TimerRefresh(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
return;
PtrTask oldTask = it->second.lock();
if (!oldTask)
{
_timers.erase(it);
return;
}
uint32_t delay = oldTask->DelayTime();
TaskFunc cb = oldTask->GetCallback();
_timers.erase(it); // 先删除旧映射
oldTask->Cancel(); // 取消旧任务
TimerAdd(id, delay, cb); // 添加新任务
}
void TimerCancel(uint64_t id)
{
auto it = _timers.find(id);
if (it == _timers.end())
{
return;
}
PtrTask pt = it->second.lock();
if (pt)
pt->Cancel();
}
void RunTimerTask()
{
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear();
}
private:
using PtrTask = std::shared_ptr<TimerTask>;
using WeakTask = std::weak_ptr<TimerTask>;
int _tick; // 指针指向的位置
int _capacity; // 数组的容量,也是支持任务的最大延迟时间
std::vector<std::vector<PtrTask>> _wheel; // 时间轮
std::unordered_map<uint64_t, WeakTask> _timers;
};
class Test
{
public:
Test() { std::cout << "构造" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
void DelTest(Test *t)
{
delete t;
}
int main()
{
Test *t = new Test();
TimerWheel tw;
tw.TimerAdd(888, 5, std::bind(&DelTest, t));
for (int i = 0; i < 5; i++)
{
sleep(1);
tw.TimerRefresh(888); // 刷新定时任务
tw.RunTimerTask(); // 向后移动秒针
std::cout << "刷新了一下定时任务,重新需要5s中后才会销毁\n";
}
tw.TimerCancel(888);
while (1)
{
sleep(1);
std::cout << "-------------------\n";
tw.RunTimerTask(); // 向后移动秒针
}
return 0;
}

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)