Timer的实现方式
Timer有多种实现方式,CyberRT采用了时间轮(TimingWheel)方式。关于Timing Wheels的详细描述可以参考附录的链接。此处不赘述。
CyberRT Timer的类构成
实现细节
Timer
Timer类是门户,但定时器真正的核心是TimingWheel。
Timer的定义相对简单,基本就是传入period(多久后触发)、callback(触发后执行的函数)、oneshot(执行一次,还是循环执行)创建定时器,然后调用Start()启动定时器。调用Stop()停止定时器。
测试样例可以再Timer_test.cc中找到。
Timer的调用Start()后,会调用InitTimerTask()创建一个新的TimerTask, 然后通过timing_wheel_->AddTask(task_)添加到时间轮对应的slot上。
TimerTask指针是时间轮调度操作的单元。Timer::InitTimerTask()函数较长,主要是对循环调用场景下task_->callback的Lambda定义。
对于循环场景,我们需要下一次调度的准确时间,所以我们需要计算函数的执行时间execute_time_ms和累积的错误时间accumulated_error_ms。
函数执行消耗时间execute_time_ms比较容易理解, 累积的错误时间accumulated_error_ms第一次看到时,容易让人疑惑。下面用一个简单的例子来解释一下。
假设定时器tick为2ms。一个循环执行的timer,每11ms执行一次。所以第一次会向上取整,落到第12ms这个tick上。到时触发后,执行耗时2ms,此时当前tick为第14ms这个tick。
我们准备重新将timer加入到定时器中,下一次执行时间该是多少呢?如果没有accumulated_error_ms,我们会计算得到 11 - 2 = 9, 使用当前时间14 + 9 = 23,向上取整的到了24。也就是说下一次会在第24ms时触发。这和我们理想情况下第二次22ms触发整整差了2ms。如果没有accumulated_error_ms,越往后累积偏差越大,这样精确性就得不到保障了。
下面我们看下考虑到accumulated_error_ms后的情况。我们通过
accumulated_error_ms += start(当前时刻) - last_execute_time_ms(执行耗时) - interval_ms = 14 - 2 - 11 = 1
的到了accumulated_error_ms=1,在随后的计算中
next_fire_duration_ms= interval_ms-execute_time_ms-accumulated_error_ms= 11 - 2 - 1 = 8
的到了下一次触发间隔时间next_fire_duration_ms=8
也就是说在14(当前时刻)+ 8 = 22ms时触发,这和我们期望的时间完全一致,时间精确性的到了最大限度地保障。
TimingWheel
CyberRT使用了两个时间轮,一个主时间轮和一个辅助时间轮。
主时间轮512个slot,tick为2ms,可以承载1024ms。
副时间轮64个slot,一个slot对应1024ms。当主时间轮运行一周后,会从副时间轮对应slot中取出task(时间范围1024ms)应用到主时间轮上,继续执行。
所以可以设置的最大间隔为 TIMER_MAX_INTERVAL_MS = 65536ms,约为65秒。
static const uint64_t WORK_WHEEL_SIZE = 512;
static const uint64_t ASSISTANT_WHEEL_SIZE = 64;
static const uint64_t TIMER_RESOLUTION_MS = 2;
static const uint64_t TIMER_MAX_INTERVAL_MS =
WORK_WHEEL_SIZE * ASSISTANT_WHEEL_SIZE * TIMER_RESOLUTION_MS; //65536,约65s左右。
TimerBucket work_wheel_[WORK_WHEEL_SIZE]; //主时间轮
TimerBucket assistant_wheel_[ASSISTANT_WHEEL_SIZE]; //辅助时间轮
Timer主线程
主线程运行TickFunc函数,代码如下:
- Tick()函数是执行当前slot中到期的所有TimerTask。
- rate.Sleep() 负责精确休眠tick时间,即2ms。
- 更新时间轮index
- 如果时间轮走过一圈,则调用Cascade()从辅助时间轮中取对应slot的TimerTask部署到主时间轮上。
AddTask函数
AddTask根据current_work_wheel_index + next_fire_duration_ms/TIMER_RESOLUTION_MS 得到对应的时间轮index。
-
如果index 没有超过WORK_WHEEL_SIZE,则直接插入到对应slot。
-
如果index超过WORK_WHEEL_SIZE,但是assistant_ticks为1并且index小于当前时间的index。
-
assistant_ticks为1且work index>= current index或者assistant_ticks > 1
示例:assistant_ticks=2,直接将任务插入到当前assistant wheel index + 2的slot中。