545
社区成员
发帖
与我相关
我的任务
分享定时器是一种用于组织和管理大量延时任务的基础模块。它允许程序在未来的某个时间点执行预先定义的操作,而无需让线程通过忙等(busy-wait)来消耗 CPU 资源。无论是操作系统内核还是应用层服务,定时器都是实现超时控制、周期性任务、心跳检测等功能的核心组件。
在软件开发中,我们经常需要处理这样的场景:
如果为每个任务启动一个独立的线程,并调用 sleep() 或 usleep(),会导致大量线程阻塞,且线程切换开销巨大,无法支持大规模并发。定时器模块的核心价值在于:用单线程(或极少的线程)高效管理成千上万个延时任务。
它通过两个关键设计实现这一目标:
定时器容器需要按时间戳有序存储任务,并支持高效的增、删、查(最近节点)。常见的选择有:
begin() 和rbegin()是 O(1))。

有了数据容器,还需要一种方式让线程在合适的时间醒来处理任务。常见的触发机制包括:
最简单的做法:每次取出最近任务的等待时间,调用 sleep(diff) 后醒来处理到期任务。但这种方式无法处理其他事件(如网络 IO),且 sleep 期间线程完全阻塞。
如 epoll_wait、select、poll 都支持传入一个超时值。将最近任务的等待时间作为超时参数,当多路复用返回时,先检查是否有 IO 事件,再处理已到期的定时器。这是单线程事件循环(如 Redis、libevent)的经典做法。
Linux 提供了 timerfd_create 接口,可以将定时器抽象为一个文件描述符。当定时器到期时,该 fd 变为可读。我们可以把这个 fd 加入到 epoll 中,实现定时器与 IO 事件的统一管理。这种方式更自然,且支持高精度(纳秒级)。
下面给出一个基于 std::multimap 和 epoll_wait 超时参数的定时器实现。该示例演示了核心思想,但实际工程中可进一步优化(如使用时间轮、timerfd 等)。
#include <iostream>
#include <sys/epoll.h>
#include <map>
#include <unistd.h>
#include <functional>
#include <memory>
#include <chrono>
#include <errno.h>
class TimerNode {
public:
friend class Timer;
TimerNode(std::function<void()> callback, uint64_t time_out)
: callback_(std::move(callback)), time_out_(time_out) {}
private:
std::function<void()> callback_;
uint64_t time_out_; // 绝对超时时间(毫秒)
};
class Timer {
public:
~Timer() {
for (auto& pair : timer_map_) {
delete pair.second; // 释放所有节点内存
}
}
// 获取当前单调时间(毫秒)
static uint64_t GetCurrentTime() {
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
// 添加一个相对超时 diff(毫秒)的任务,返回节点指针以便删除
TimerNode* AddTimeout(uint64_t diff, std::function<void()> cb) {
auto node = new TimerNode(std::move(cb), GetCurrentTime() + diff);
// 插入到 multimap 中,优化插入位置:如果新节点超时时间最大,则使用 hint 插入末尾
if (timer_map_.empty() || node->time_out_ >= timer_map_.rbegin()->first) {
// 使用 end() 作为 hint 插入末尾(对于 multitimap,end() 是合法 hint)
auto it = timer_map_.emplace_hint(timer_map_.end(),
std::make_pair(node->time_out_, node));
return it->second;
} else {
auto it = timer_map_.insert(std::make_pair(node->time_out_, node));
return it->second;
}
}
// 删除指定的定时器节点(同时释放内存)
void DelTimeout(TimerNode* node) {
if (!node) return;
auto range = timer_map_.equal_range(node->time_out_);
for (auto it = range.first; it != range.second; ++it) {
if (it->second == node) {
timer_map_.erase(it);
delete node; // 注意:node 被删除后外部不应再使用
break;
}
}
}
// 获取距离下一个任务到期的等待时间(毫秒),若无任务返回 -1
int WaitTime() {
if (timer_map_.empty()) return -1;
uint64_t now = GetCurrentTime();
uint64_t next = timer_map_.begin()->first;
int64_t diff = next - now;
return diff > 0 ? diff : 0; // 如果已过期,立即返回 0
}
// 处理所有已到期的任务
void HandleTimeout() {
uint64_t now = GetCurrentTime();
auto it = timer_map_.begin();
while (it != timer_map_.end() && it->first <= now) {
it->second->callback_(); // 执行回调
delete it->second; // 释放节点
it = timer_map_.erase(it); // 移除并指向下一个
}
}
private:
std::multimap<uint64_t, TimerNode*> timer_map_;
};
int main() {
int epfd = epoll_create1(0);
if (epfd == -1) {
std::cerr << "epoll_create error" << std::endl;
return -1;
}
Timer timer;
int counter = 0;
// 添加几个定时任务
timer.AddTimeout(1000, [&]() {
std::cout << "timeout 1: " << counter++ << std::endl;
});
timer.AddTimeout(2000, [&]() {
std::cout << "timeout 2: " << counter++ << std::endl;
});
auto node = timer.AddTimeout(3000, [&]() {
std::cout << "timeout 3: " << counter++ << std::endl;
});
// 删除第三个任务
timer.DelTimeout(node);
epoll_event events[512];
while (true) {
int n = epoll_wait(epfd, events, 512, timer.WaitTime());
if (n == -1) {
if (errno == EINTR) continue; // 被信号中断,继续
std::cerr << "epoll_wait error" << std::endl;
break;
}
// 这里可以处理 IO 事件(events),本示例无注册 fd
// 处理到期的定时任务
timer.HandleTimeout();
}
close(epfd);
return 0;
}
std::multimap**:允许同一时刻有多个任务到期(键值重复)。emplace_hint 插入末尾,提高性能。multimap 中移除并 delete,避免内存泄漏。WaitTime() 返回下一个任务剩余毫秒数,作为 epoll_wait 的超时参数。Linux 下更优雅的做法是使用 timerfd,将每个定时器变为一个 fd,然后统一放入 epoll。这样就不需要手动计算超时时间,而且可以同时处理多个定时器。但注意每个 timerfd 对应一个定时器,若有大量任务则需管理多个 fd。一个折衷是只用一个 timerfd 设置为最近任务的到期时间,到期后再重新设置,类似 epoll 超时参数的思想。
当定时器任务可能耗时较长时,通常将任务投递给线程池执行,以免阻塞事件循环。同时需要保护共享容器(如 multimap)的并发访问,可以使用互斥锁或设计为无锁结构。
对于海量连接(如百万级),红黑树 O(log n) 的插入删除开销变得明显。时间轮将时间离散化,插入删除均为 O(1),成为网络框架的首选(如 Netty、Kafka)。多层时间轮可以支持任意时间跨度的任务。
GetCurrentTime。timer_create,它基于信号或线程通知,精度更高。定时器设计是后端开发和系统编程的基础技能。一个优秀的定时器模块需要在数据结构、触发机制、并发模型之间做出权衡。
https://blog.csdn.net/qq_57951250/article/details/159047643?sharetype=blogdetail&sharerId=159047643&sharerefer=PC&sharesource=qq_57951250&spm=1011.2480.3001.8118