定时器的介绍与实现

一、定时器介绍

定时器是管理大量延时任务的模块,常见可以用红黑树等数据结构作为容器组织所有延时任务,通过超时检测来触发并执行任务(可以理解为异步非阻塞)

触发时刻作为Key,延时任务作为Val(需要允许Key重复),本实现中选择STL中的multimap(底层是红黑树)

二、定时器设计

2.1 设计延时任务

设计任务类 TimerTask,字段包括:

  • 添加时间 uint32_t addTime_
  • 执行时间 uint32_t execTime_ (表示什么时候应该执行)
  • 异步回调函数 Callback func_:用函数对象function<void(TimerTask* task)>进行描述(支持lambda表达式)

为了操作这些字段,设计以下公有方法:

  • AddTime():用于获取添加时间
  • ExecTime():用于获取执行时间

TimerTask的异步回调函数func_的运行我们不希望暴露给用户,应该交由定时器同一管理,所以run()方法设计为私有,并且通过将Timer类声明为友元以方便调用run()

cpp 复制代码
class TimerTask {
    friend class Timer;
    using Callback = function<void(TimerTask*)>;
public:
    TimerTask(uint32_t addTime, uint32_t execTime, Callback func) {
        addTime_ = addTime;
        execTime_ = execTime;
        func_ = move(func);
    }    

    void AddTime() const {
        return addTime_;
    }

    void ExecTime() const {
        return execTime_;
    }    

private:
    void run() {
        func_(this);
    }
    Callback func_;
    uint32_t addTime_;
    uint32_t execTime_;
};

2.2 设计定时器

先介绍定时器的容器+驱动方式

1.容器 :本设计中数据结构选择multimap timeouts,作为私有成员

为了用户可以使用,需要以下公有方法:

  • AddTimeout:向数据结构中添加任务
  • DelTimeout:向数据结构中
  • Update:触发延时任务,调用时机由外部控制

并且,为了知道当前的时刻(相对于系统启动时),还需要额外一个GetTick()方法

2.驱动方式 :用事件驱动的方式来触发,用epoll_wait来检测超时事件发生,设置epoll_wait的第四个参数------超时参数。为了外部的epoll实例能够知道超时时间,timer还需要提供一个GetWaitTime()方法

cpp 复制代码
class Timer {
    using Seconds = chrono::seconds;
public:
    uint32_t GetTick() const {
        auto now = chrono::steady_clock::now(); // 当前时刻(time_point)
        auto sc = chrono::duration_cast<Seconds>(now.time_since_epoch()); //带上模板参数
        return sc.count();
    }

    TimerTask* AddTimeout(uint32_t offset, TimerTask::Callback func) {
        uint32_t addTime = GetTick();
        uint32_t execTime = addTime + offset;
        auto task = new TimerTask(addTime, execTime, move(func));
        auto ele = timeouts_.emplace(execTime, task);
        return ele->second;
    }

    void DelTimeout(TimerTask* task) {
        if (!task) return;
        auto range = timeouts_.equal_range(task->ExecTime());
        for (auto it = range.first; it != range.second;) {
            if (it->second == task) {
                it = timeouts_.erase(it);
            } else {
                ++it;
            }
        }
    }

    void Update(uint32_t now) {
        auto it = timeouts_.begin();
        while (it != timeouts_.end() && it->first <= now) {
            it->second->run();
            delete(it->second);
            it = timeouts_.erase(it);
        }
    }

    int GetWaitTime() {
        if (timeouts_.empty()) {
            return -1;
        }
        uint32_t now = GetTick();
        auto it = timeouts_.begin();
        
        if (it->first <= now) {
            return 0;
        }

        uint32_t diff = it->first - now;
        return (int)diff;
    }
private:
    multimap<uint32_t, TimerTask*> timeouts_;
};

三、使用定时器

这里我们使用epoll_wait,模仿网络编程的方式来驱动timer,先用epoll_create创建epoll实例,然后通过调用AddTimeout方法添加延时任务,添加时用lambda封装,在主循环中,调用GetWaitTime方法获取最近需要等待的时间来设置epoll_wait的第四个超时参数(达成异步非阻塞执行),时机执行超时任务由Update承担

cpp 复制代码
// ============ 主程序 ============
int main() {
    // 1. 创建 epoll 实例
    int epfd = epoll_create(1);
    if (epfd < 0) {
        perror("epoll_create");
        return -1;
    }
    
    // 2. 创建定时器
    Timer timer;
    
    // 3. 添加定时任务
    cout << "开始定时器循环...\n";
    cout << "添加定时任务:\n";
    
    timer.AddTimeout(3, [](TimerTask* task) { 
        cout << "3秒到了!\n"; 
    });
    
    timer.AddTimeout(5, [](TimerTask* task) { 
        cout << "5秒到了!\n"; 
    });
    
    timer.AddTimeout(2, [](TimerTask* task) { 
        cout << "2秒到了!\n"; 
    });
    
    // 4. epoll 事件数组
    epoll_event events[64] = {0};
    
    // 5. 主循环
    while (true) {
        // 5.1 获取需要等待的时间
        int waitSeconds = timer.GetWaitTime();
        if (waitSeconds == -1) {
            cout << "所有定时任务执行完毕,退出\n";
            break;
        }
        
        // 5.2 epoll_wait 驱动(超时时间:秒 → 毫秒)
        int n = epoll_wait(epfd, events, 64, waitSeconds * 1000);
        
        // 5.3 处理网络事件(如果有)
        if (n < 0) {
            if (errno == EINTR) continue;
            perror("epoll_wait");
            break;
        } else if (n > 0) {
            // 有网络事件到达,可以处理
            // for (int i = 0; i < n; i++) {
            //     // 处理 events[i]
            // }
        }
        
        // 5.4 获取当前时间并更新定时器
        timer.Update(Timer::GetTick());
    }
    
    // 6. 清理
    close(epfd);
    return 0;
}