从零手写高性能 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;
}

Logo

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

更多推荐