一、定时器介绍
定时器是管理大量延时任务的模块,常见可以用红黑树等数据结构作为容器组织所有延时任务,通过超时检测来触发并执行任务(可以理解为异步非阻塞)
触发时刻作为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;
}