1、定时器介绍
- 定时器其实和闹钟差不多,通过定时器可以给服务器注册定时事件,这是服务器上经常要处理的一类事件。定时器的实现依赖于Linux提供的定时机制,目前的方式有:1)alarm()或setitimer(),这俩的本质都是先设置一个超时时间,然后等SIGALARM信号触发,通过捕获信号来判断超时 2) 套接字超时选项,对应SO_RECVTIMEO和SO_SNDTIMEO,通过errno来判断超时 3)多路复用超时参数,select/poll/epoll都支持设置超时参数,通过判断返回值为0来判断超时
- 服务器程序通常需要处理众多定时事件,如何有效地组织与管理这些定时事件对服务器的性能至关重要。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间堆,将所有定时器组织起来,以实现对定时事件的统一管理。每个定时器通常至少包含两个成员:一个超时时间(相对时间或绝对时间)和一个任务回调函数。除此外,定时器还可以包括回调函数参数及是否自动重启等信息。
- 基于升序链表的定时器
1) 所有定时器组织成链表结构,链表成员包含超时时间,回调函数,回调函数参数,以及链表指针域。
2) 定时器在链表中按超时时间进行升序排列,超时时间短的在前,长的在后。每次添加定时器时,都要按超时时间将定时器插入到链表的指定位置。
3)程序运行后维护一个周期性触发的tick信号,比如利用alarm函数周期性触发ALARM信号,在信号处理函数中从头遍历定时器链表,判断定时器是否超时。如果定时器超时,则记录下该定时器,然后将其从链表中删除。
4)执行所有超时的定时器的回调函数。
以上就是一个基于升序链表的定时器实现,这种方式添加定时器的时间复杂度是O(n),删除定时器的时间复杂度是O(1),执行定时任务的时间复杂度是O(1)。tick信号的周期对定时器的性能有较大的影响,当tick信号周期较小时,定时器精度高,但CPU负担较高,因为要频繁执行信号处理函数;当tick信号周期较大时,CPU负担小,但定时精度差。当定时器数量较多时,链表插入操作开销比较大。 - 时间堆定时器
时间堆定时器直接将超时时间当作tick周期,具体操作是每次都取出所有定时器中超时时间最小的超时值作为一个tick,这样,一旦tick触发,超时时间最小的定时器必然到期。处理完已超时的定时器后,再从剩余的定时器中找出超时时间最小的一个,并将这个最小时间作为下一个tick,如此反复,就可以实现较为精确的定时。最小堆很适合处理这种定时方案,将所有定时器按最小堆来组织,可以很方便地获取到当前的最小超时时间,sylar采取的即是这种方案。这种方式用堆管理定时器,效率比较高。
还有一种基于hash的时间轮定时器实现方法,后续补充。
2、sylar框架定时器设计思路 - sylar的定时器采用最小堆设计,所有定时器根据绝对的超时时间点进行排序,每次取出离当前时间最近的一个超时时间点,计算出超时需要等待的时间,然后等待超时。超时时间到后,获取当前的绝对时间点,然后把最小堆里超时时间点小于这个时间点的定时器都收集起来,执行它们的回调函数。注意,在注册定时事件时,一般提供的是相对时间,比如相对当前时间3秒后执行。sylar会根据传入的相对时间和当前的绝对时间计算出定时器超时时的绝对时间点,然后根据这个绝对时间点对定时器进行最小堆排序。sylar定时器的超时等待基于epoll_wait,精度只支持毫秒级,因为epoll_wait的超时精度也只有毫秒级。
3、定时器与IOManager整合 - 关于定时器和IO协程调度器的整合。IO协程调度器的idle协程会在调度器空闲时阻塞在epoll_wait上,等待IO事件发生。在之前的代码里,epoll_wait具有固定的超时时间,这个值是5秒钟。加入定时器功能后,epoll_wait的超时时间改用当前定时器的最小超时时间来代替。epoll_wait返回后,根据当前的绝对时间把已超时的所有定时器收集起来,执行它们的回调函数。由于epoll_wait的返回并不一定是超时引起的,也有可能是IO事件唤醒的,所以在epoll_wait返回后不能想当然地假设定时器已经超时了,而是要再判断一下定时器有没有超时,这时绝对时间的好处就体现出来了,通过比较当前的绝对时间和定时器的绝对超时时间,就可以确定一个定时器到底有没有超时。
4、代码实现 - Timer类
/**
* @brief 定时器
*/
class Timer : public std::enable_shared_from_this<Timer> {
friend class TimerManager;
public:
/// 定时器的智能指针类型
typedef std::shared_ptr<Timer> ptr;
/**
* @brief 取消定时器
*/
bool cancel();
/**
* @brief 刷新设置定时器的执行时间
*/
bool refresh();
/**
* @brief 重置定时器时间
* @param[in] ms 定时器执行间隔时间(毫秒)
* @param[in] from_now 是否从当前时间开始计算
*/
bool reset(uint64_t ms, bool from_now);
private:
/**
* @brief 构造函数
* @param[in] ms 定时器执行间隔时间
* @param[in] cb 回调函数
* @param[in] recurring 是否循环
* @param[in] manager 定时器管理器
*/
Timer(uint64_t ms, std::function<void()> cb,
bool recurring, TimerManager* manager);
/**
* @brief 构造函数
* @param[in] next 执行的时间戳(毫秒)
*/
Timer(uint64_t next);
private:
/// 是否循环定时器
bool m_recurring = false;
/// 执行周期
uint64_t m_ms = 0;
/// 精确的执行时间
uint64_t m_next = 0;
/// 回调函数
std::function<void()> m_cb;
/// 定时器管理器
TimerManager* m_manager = nullptr;
private:
/**
* @brief 定时器比较仿函数
*/
struct Comparator {
/**
* @brief 比较定时器的智能指针的大小(按执行时间排序)
* @param[in] lhs 定时器智能指针
* @param[in] rhs 定时器智能指针
*/
bool operator()(const Timer::ptr& lhs, const Timer::ptr& rhs) const;
};
};
- TimerManager类
所有的Timer对象都由TimerManager类进行管理,TimerManager包含一个std::set类型的Timer集合,这个集合就是定时器的最小堆结构,因为set里的元素总是排序过的,所以总是可以很方便地获取到当前的最小定时器。TimerManager提供创建定时器,获取最近一个定时器的超时时间,以及获取全部已经超时的定时器回调函数的方法,并且提供了一个onTimerInsertedAtFront()方法,这是一个虚函数,由IOManager继承时实现,当新的定时器插入到Timer集合的首部时,TimerManager通过该方法来通知IOManager立刻更新当前的epoll_wait超时。TimerManager还负责检测是否发生了校时,由detectClockRollover方法实现。
/**
* @brief 定时器管理器
*/
class TimerManager {
friend class Timer;
public:
/// 读写锁类型
typedef RWMutex RWMutexType;
/**
* @brief 构造函数
*/
TimerManager();
/**
* @brief 析构函数
*/
virtual ~TimerManager();
/**
* @brief 添加定时器
* @param[in] ms 定时器执行间隔时间
* @param[in] cb 定时器回调函数
* @param[in] recurring 是否循环定时器
*/
Timer::ptr addTimer(uint64_t ms, std::function<void()> cb
,bool recurring = false);
/**
* @brief 添加条件定时器
* @param[in] ms 定时器执行间隔时间
* @param[in] cb 定时器回调函数
* @param[in] weak_cond 条件
* @param[in] recurring 是否循环
*/
Timer::ptr addConditionTimer(uint64_t ms, std::function<void()> cb
,std::weak_ptr<void> weak_cond
,bool recurring = false);
/**
* @brief 到最近一个定时器执行的时间间隔(毫秒)
*/
uint64_t getNextTimer();
/**
* @brief 获取需要执行的定时器的回调函数列表
* @param[out] cbs 回调函数数组
*/
void listExpiredCb(std::vector<std::function<void()> >& cbs);
/**
* @brief 是否有定时器
*/
bool hasTimer();
protected:
/**
* @brief 当有新的定时器插入到定时器的首部,执行该函数
*/
virtual void onTimerInsertedAtFront() = 0;
/**
* @brief 将定时器添加到管理器中
*/
void addTimer(Timer::ptr val, RWMutexType::WriteLock& lock);
private:
/**
* @brief 检测服务器时间是否被调后了
*/
bool detectClockRollover(uint64_t now_ms);
private:
/// Mutex
RWMutexType m_mutex;
/// 定时器集合
std::set<Timer::ptr, Timer::Comparator> m_timers;
/// 是否触发onTimerInsertedAtFront
bool m_tickled = false;
/// 上次执行时间
uint64_t m_previouseTime = 0;
};
- IOManager与定时器整合
class IOManager : public Scheduler, public TimerManager {
...
}
void IOManager::idle() {
SYLAR_LOG_DEBUG(g_logger) << "idle";
// 一次epoll_wait最多检测256个就绪事件,如果就绪事件超过了这个数,那么会在下轮epoll_wati继续处理
const uint64_t MAX_EVNETS = 256;
epoll_event *events = new epoll_event[MAX_EVNETS]();
std::shared_ptr<epoll_event> shared_events(events, [](epoll_event *ptr) {
delete[] ptr;
});
while (true) {
// 获取下一个定时器的超时时间,顺便判断调度器是否停止
uint64_t next_timeout = 0;
if( SYLAR_UNLIKELY(stopping(next_timeout))) {
SYLAR_LOG_DEBUG(g_logger) << "name=" << getName() << "idle stopping exit";
break;
}
// 阻塞在epoll_wait上,等待事件发生或定时器超时
int rt = 0;
do{
// 默认超时时间5秒,如果下一个定时器的超时时间大于5秒,仍以5秒来计算超时,避免定时器超时时间太大时,epoll_wait一直阻塞
static const int MAX_TIMEOUT = 5000;
if(next_timeout != ~0ull) {
next_timeout = std::min((int)next_timeout, MAX_TIMEOUT);
} else {
next_timeout = MAX_TIMEOUT;
}
rt = epoll_wait(m_epfd, events, MAX_EVNETS, (int)next_timeout);
if(rt < 0 && errno == EINTR) {
continue;
} else {
break;
}
} while(true);
// 收集所有已超时的定时器,执行回调函数
std::vector<std::function<void()>> cbs;
listExpiredCb(cbs);
if(!cbs.empty()) {
for(const auto &cb : cbs) {
schedule(cb);
}
cbs.clear();
}
...
}