【动手写协程库 3】定时器

【动手写协程库】系列笔记是学习sylar的协程库时的记录,参考了从零开始重写sylar C++高性能分布式服务器框架和代码随想录中的文档。文章并不是对所有代码的详细解释,而是为了自己理解一些片段所做的笔记。

TimerManager类中具体定义实现可以在这里查看:Github: src/timer.cpp

通过定时器,我们可以实现给服务器注册定时事件。sylar的定时器采用最小堆设计,所有定时器根据绝对的超时时间点(也就是超时到期的具体时间戳)进行排序,每次取出离当前时间最近的一个超时时间点,计算出超时需要等待的时间,然后等待超时。超时时间到后,获取当前的绝对时间点,然后把最小堆里超时时间点小于这个时间点的定时器都收集起来,执行它们的回调函数。

定时器相关API如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Timer : public std::enable_shared_from_this<Timer> {
friend class TimerManager;

public:
using Ptr = std::shared_ptr<Timer>;

bool Cancel();
bool Refresh();
bool Reset(uint64_t ms, bool from_now);

private:
Timer(uint64_t ms, std::function<void()> cb, bool recur, TimerManager* manager);
Timer(uint64_t next);

private:
bool is_recur_; // 是否循环定时器
uint64_t exec_cycle_; // 执行周期
uint64_t next_; // 下一次的到期时间
std::function<void()> callback_;
TimerManager* manager_;

struct Comp {
bool operator()(const Timer::Ptr& lt, const Timer::Ptr& rt) const {
if (!lt || !rt) {
return !lt && rt;
}
return lt->next_ < rt->next_;
}
};
};

class TimerManager {
friend class Timer;

public:
TimerManager();
virtual ~TimerManager();

// public 添加定时器
Timer::Ptr AddTimer(uint64_t ms, std::function<void()> cb, bool is_recur = false);

// 添加条件定时器,如果条件成立则定时器才有效
Timer::Ptr AddConditionTimer(uint64_t ms, std::function<void()> cb, std::weak_ptr<void> cond, bool is_recur = false);

// 获取下一个定时器到现在的执行间隔时间
// 如果没有定时器了,就返回uint64_t的最大值
uint64_t GetNextTimerInterval();

// 获取需要执行的定时器的回调函数列表
std::vector<std::function<void()>> GetExpiredCbList();

// 是否还有定时器
bool HasTimer();

protected:
virtual void OnTimerInsertAtFront() = 0;

void AddTimer(Timer::Ptr timer, std::shared_lock<std::shared_mutex>& lock);

private:
// 系统时钟是否出现了回绕(rollover)现象,即当前时间比之前记录的时间要小很多
// 用于检测服务器时间是否被调后了
bool DetectClockRollover(uint64_t now_ms);

private:
std::shared_mutex rw_mutex_;
std::set<Timer::Ptr, Timer::Comp> timer_heap_;
bool is_tickled_;
uint64_t pre_exec_time_;
};

个人感觉最重要的API是AddTimerGetNextTimerIntervalGetExpiredCbList

  • AddTimer向时间堆中添加超时超时时间到了后的回调函数(利用Timer类来封装)。
  • GetNextTimerInterval用于获取下一个定时器到现在的执行间隔时间,这会用于IOManager::Idle()中用于与规定的最大超时时间进行比较,用较小者作为epoll_wait的超时时间参数。
  • GetExpiredCbList获取的是所有超时定时器的回调函数。在IOManager::Idle()会调用这个函数将所有超时定时器的回调函数作为调度任务加入任务队列进行处理。

在定时器中,使用GetElapsedMS()来获取系统自启动来经过的时间,其内部使用clock_gettime来获取时间,这相比于一些传统时间获取函数(如timegettimeofday)有更高的精度:

1
2
3
4
5
6
// 获取系统自启动来经过的时间
static uint64_t GetElapsedMS() {
struct timespec ts = {0};
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}