Linux高级--3.2.4.2 常见的几种timer的设计方案
1. 使用 STL Map 管理用户定时器与 epoll_wait
配合
设计思路:
该方案的核心思想是使用 std::map
或 std::multimap
来管理定时器,根据时间戳的顺序,依次添加用户注册的定时器任务。当定时器触发时,执行对应的回调函数。为了高效地管理定时器,epoll_wait
用于监听 I/O 事件,包括定时器的超时事件。
具体实现:
- STL Map 管理定时器:
- 用户注册的定时器按照到期时间存储在
std::map
中。std::map
会根据时间戳自动进行排序,确保我们每次都能取出最早超时的定时器。 - 定时器的超时时间由用户设置,一旦到达该时间点,
epoll_wait
会触发超时事件。
- 用户注册的定时器按照到期时间存储在
- 与
epoll_wait
配合:- 将当前最小超时时间的定时器加入到
epoll_wait
监听列表。 epoll_wait
将阻塞,直到某个定时器超时,返回事件。- 一旦
epoll_wait
返回,取出超时事件并执行对应的回调函数。 - 重新设置
epoll_wait
,并将下一个最小的超时时间添加到epoll_wait
中。
- 将当前最小超时时间的定时器加入到
优点:
- 通过
std::map
保证定时器按时间排序,获取最小超时时间非常高效。 - 可以很方便地扩展,支持多种不同类型的事件监听(如文件描述符的事件)。
缺点:
- 每次添加定时器时,都需要通过
epoll_wait
更新超时时间,可能会增加额外的开销。 - 如果定时器数量非常多,
std::map
的时间复杂度为O(log N)
,可能导致性能瓶颈。
这个示例代码尽量简单的展示了timer的实现思想,方便理解。真正的项目使用需要自己优化重新封装。
#include <iostream>
#include <chrono>
#include <memory>
#include <map>
#include <string>
#include <unistd.h>
#include "libtimer.h"
#include <sys/epoll.h>
#include <pthread.h>#define OPEN_LOG#ifdef OPEN_LOG#define log(cnt, ...) fprintf(stdout, "[TIMER][%s][%d]: " cnt "\n", __func__, __LINE__, ##__VA_ARGS__)
#else#define log(cnt, ...) NULL
#endiftypedef void (*timer_handle_type)(int);struct timer_info {timer_info(int key, int ndly, timer_handle_type cb) :id(key), ndealy(ndly), fun_cb(cb) {};int id;time_t ndealy; //msbool operator<(const struct timer_info& other) const {if (ndealy < other.ndealy) return true;else if (ndealy > other.ndealy) return false;else return id < other.id; // 可以允许插入相同元素}// 添加==操作符重载bool operator==(const timer_info& other) const {return id == other.id && ndealy == other.ndealy;}void (*fun_cb)(int);timer_info() = delete;
};
int epoll_fd = -1;
std::map<timer_info, time_t> timer_map;
int g_timer_id = 0;
pthread_t g_tid = -1;static time_t GetTick() {auto sc = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now());auto temp = std::chrono::duration_cast<std::chrono::milliseconds>(sc.time_since_epoch());return temp.count();
}void timer_moniter(void) {while (1) {auto tmp = timer_map.begin();if (tmp == timer_map.end()) continue;struct epoll_event events[2];time_t dely = tmp->first.ndealy - GetTick();int nfds = epoll_wait(epoll_fd, events, 2, dely > 0 ? dely : 0);if (nfds == -1) {log("epoll_wait err");continue;} else if (nfds == 0) {tmp->first.fun_cb(tmp->first.id);timer_map.erase(tmp);}}return;
}int timer_create(void) {epoll_fd = epoll_create(10);if (epoll_fd == -1) {log("epoll create err");return -1;}for (auto iter : timer_map) {timer_map.erase(iter.first);}return 0;
}int timer_add(time_t ndealy, timer_handle_type cb) {timer_map.emplace(timer_info(g_timer_id, ndealy + GetTick(), cb), ndealy);log("timer size: %lu, %ld", timer_map.size(), ndealy + GetTick());return g_timer_id++;
}void timer_handle_f(int id) {log("this is the timer callbck: %d", id);return;
}int main(void) {int ret = timer_create();if (ret < 0) return 1;timer_add(100, timer_handle_f);timer_add(200, timer_handle_f);timer_add(1000, timer_handle_f);timer_moniter();
}
$ ./a.out
[TIMER][timer_add][84]: timer size: 1, 13666513
[TIMER][timer_add][84]: timer size: 2, 13666613
[TIMER][timer_add][84]: timer size: 3, 13667413
[TIMER][timer_handle_f][90]: this is the timer callbck: 0
[TIMER][timer_handle_f][90]: this is the timer callbck: 1
[TIMER][timer_handle_f][90]: this is the timer callbck: 2
^C
2. 使用 timer_fd
管理定时器与 epoll_wait
配合
设计思路:
该方案的关键是使用 timerfd_create
创建一个定时器文件描述符(timer_fd
)。定时器文件描述符的超时时间与 epoll_wait
配合使用,监听定时器超时事件并执行回调函数。
具体实现:
- 创建定时器文件描述符:
- 使用
timerfd_create()
创建一个timer_fd
,并设置其超时时间。 timerfd_settime()
用于修改定时器的超时时间。
- 使用
- 管理定时器:
- 用户的定时器会按照超时时间加入到一个
std::map
中,map
存储定时器的到期时间和回调函数。 - 每次取出最小超时时间的定时器,设置到
timer_fd
中,并重新将timer_fd
加入到epoll
中监听。
- 用户的定时器会按照超时时间加入到一个
- 执行回调:
- 当
epoll_wait
返回时,检查timer_fd
是否触发,如果是,则执行相应的回调函数。 - 重新设置
timer_fd
,并将新的超时时间加入epoll_wait
中,保证系统继续运行。
- 当
优点:
- 只有一个
timer_fd
文件描述符,避免了管理多个文件描述符的复杂性。 - 通过
timerfd
提供的原生 API,能够高效地管理定时器。 - 更适合高频繁定时器或较小规模的定时任务。
缺点:
- 每次需要更新
timer_fd
的超时时间,可能会导致一些系统调用开销。 timer_fd
只适合单个定时器或多个定时器相同的超时时间。对于大量的定时器,管理复杂度较高。
#include <iostream>
#include <chrono>
#include <memory>
#include <map>
#include <string>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <pthread.h>#define OPEN_LOG#ifdef OPEN_LOG#define log(cnt, ...) fprintf(stdout, "[TIMER][%s][%d]: " cnt "\n", __func__, __LINE__, ##__VA_ARGS__)
#else#define log(cnt, ...) NULL
#endiftypedef void (*timer_handle_type)(int);struct timer_info {timer_info(int key, int ndly, timer_handle_type cb) :id(key), ndealy(ndly), fun_cb(cb) {};int id;time_t ndealy; //msbool operator<(const struct timer_info& other) const {if (ndealy < other.ndealy) return true;else if (ndealy > other.ndealy) return false;else return id < other.id; // 可以允许插入相同元素}// 添加==操作符重载bool operator==(const timer_info& other) const {return id == other.id && ndealy == other.ndealy;}void (*fun_cb)(int);timer_info() = delete;
};
int epoll_fd = -1;
std::map<timer_info, time_t> timer_map;
int g_timer_id = 0;
int timer_fd = -1;
pthread_t g_tid = -1;static time_t GetTick() {auto sc = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now());auto temp = std::chrono::duration_cast<std::chrono::milliseconds>(sc.time_since_epoch());return temp.count();
}int update_timer_fd(time_t dely) {// int timerfd_gettime(int fd, struct itimerspec *curr_valuem);struct itimerspec new_value;new_value.it_interval.tv_nsec = 0;new_value.it_interval.tv_sec = 0;new_value.it_value.tv_sec = dely / 1000;new_value.it_value.tv_nsec = (dely % 1000) * 1000 * 1000;if (new_value.it_value.tv_nsec >= 1000000000) {new_value.it_value.tv_nsec -= 1000000000;new_value.it_value.tv_sec += 1;}int ret = timerfd_settime(timer_fd, TFD_TIMER_ABSTIME, &new_value, NULL);if (ret == -1) {log("timerfd_settime err");return -1;}return 0;
}void timer_moniter(void) {while (1) {auto tmp = timer_map.begin();if (tmp == timer_map.end()) continue;struct epoll_event events[1];time_t dely = tmp->first.ndealy;update_timer_fd(dely);int nfds = epoll_wait(epoll_fd, events, 1, -1);if (nfds == 1 && events[0].data.fd == timer_fd) {tmp->first.fun_cb(tmp->first.id);timer_map.erase(tmp);} else {log("epoll_wait err");continue;}}return;
}int timer_create(void) {epoll_fd = epoll_create(10);if (epoll_fd == -1) {log("epoll create err");return -1;}timer_fd = timerfd_create(CLOCK_MONOTONIC, 0);if (timer_fd == -1) {log("timerfd_create err");return -1;}struct epoll_event ep_ev;ep_ev.data.fd = timer_fd;ep_ev.events = EPOLLIN | EPOLLET;int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, timer_fd, &ep_ev);if (ret == -1) {log("epoll_ctl failed: %s", strerror(errno));return -1;}for (auto iter : timer_map) {timer_map.erase(iter.first);}return 0;
}int timer_add(time_t ndealy, timer_handle_type cb) {timer_map.emplace(timer_info(g_timer_id, ndealy + GetTick(), cb), ndealy);log("timer size: %lu, %ld", timer_map.size(), ndealy + GetTick());return g_timer_id++;
}void timer_handle_f(int id) {log("this is the timer callbck: %d", id);return;
}int main(void) {int ret = timer_create();if (ret < 0) return 1;timer_add(100, timer_handle_f);timer_add(200, timer_handle_f);timer_add(1000, timer_handle_f);timer_moniter();
}
$ ./a.out
[TIMER][timer_add][119]: timer size: 1, 20949087
[TIMER][timer_add][119]: timer size: 2, 20949187
[TIMER][timer_add][119]: timer size: 3, 20949987
[TIMER][timer_handle_f][125]: this is the timer callbck: 0
[TIMER][timer_handle_f][125]: this is the timer callbck: 1
[TIMER][timer_handle_f][125]: this is the timer callbck: 2
^C
3. 每个定时器都有独立的 timer_fd
,并通过 epoll
监听
设计思路:
该方案是将每个定时器都创建为一个独立的 timer_fd
文件描述符,并将其与回调函数一一绑定。使用 epoll
监听这些定时器文件描述符的超时事件。当某个定时器超时时,epoll_wait
会返回事件,并通过 epoll_data
的 ptr
字段找到对应的回调函数,执行任务。
具体实现:
- 每个定时器创建
timer_fd
:- 用户注册的每个定时器都会通过
timerfd_create
创建一个独立的文件描述符。 - 每个
timer_fd
都设置自己的超时时间。
- 用户注册的每个定时器都会通过
- 与
epoll
配合:- 每个
timer_fd
文件描述符都被加入到epoll
监听列表中。 - 每个定时器的回调函数会通过
epoll_data.ptr
字段存储。
- 每个
- 触发回调:
- 当
epoll_wait
返回时,检查各个timer_fd
是否超时,若超时则通过epoll_data.ptr
找到回调函数并执行。
- 当
- 重新设置定时器:
- 执行完回调后,重新设置
timer_fd
的超时时间,确保定时器继续生效。
- 执行完回调后,重新设置
优点:
- 每个定时器都有独立的
timer_fd
,便于单独管理。 - 高效的事件处理机制,
epoll
是 Linux 高效的 I/O 多路复用机制。
缺点:
- 当有大量定时器时,每个定时器都创建一个
timer_fd
会增加系统的资源开销。 - 定时器数量增加时,
epoll
的性能可能受到影响。
4. 时间轮(Time Wheel)设计方案
设计思路:
时间轮是一种高效的定时器管理方案,适合大量定时器的管理。其基本思想是将时间划分为多个槽,每个槽代表一个固定的时间单元。当定时器被添加时,根据定时器的超时时间将其放入相应的槽中。每当轮到该时间槽时,系统就会遍历槽中的定时器并执行回调。
具体实现:
-
时间轮的结构:
- 时间轮由多个层次组成,顶层代表更长时间间隔,下一层代表更短时间间隔。
- 每一层是一个指针数组,指向下一级时间段的任务队列。
- 定时器的超时时间是通过
tickcount
(时间轮的当前指针位置)不断推进的。
-
时间轮的工作原理:
- 时间轮按“tick”的方式推进,每次推进一个单位,检查当前槽的任务是否需要执行。
- 如果某个任务需要执行,它会被移到上一层的时间槽,并将其时间间隔减去当前时间槽的长度。
- 通过分层设计,时间轮能够高效地管理大量定时器,避免了遍历所有定时器的开销。
-
定时器添加:
- 将定时器的到期时间映射到对应的时间轮槽中,时间轮会根据当前时间指针的位置,确定该任务应该被放置在哪一层。
优点:
- 高效管理大量定时器,避免了频繁遍历所有定时器的问题。
- 时间轮的复杂度是常数时间
O(1)
,比起std::map
需要O(log N)
的时间复杂度要高效。
缺点:
- 适用于定时器数量非常多且时间间隔固定的场景,对于复杂的定时器管理需求可能不够灵活。
- 时间轮设计较为复杂,难度较高。
总结与比较:
方案 | 优点 | 缺点 |
---|---|---|
方案 1 | 简单易实现,利用 map 管理定时器,灵活 | 可能存在性能瓶颈,尤其是大量定时器时 |
方案 2 | 一个 timer_fd 管理所有定时器,简化管理 | 需要频繁更新 timer_fd ,性能瓶颈 |
方案 3 | 每个定时器都有独立 timer_fd ,精确管理 | 定时器过多时资源消耗大 |
方案 4 | 高效管理大量定时器,适合大规模定时任务 | 设计较为复杂,难以处理复杂场景 |
推荐:
- 对于低频定时器或定时器数量较少的应用,方案 1 和 方案 2 都是不错的选择。
- 对于高频定时器或大量定时器的情况,建议使用 方案 4,即 时间轮,能更高效地处理定时器。
0voice · GitHub