定时器的实现
Go的定时器基于小顶堆(heap)数据结构和Goroutine 机制来高效管理定时任务。定时器的核心思想是尽可能减少不必要的轮询,并且利用堆来保持定时任务的有序性
- 定时器的核心数据结构 Golang 定时器的核心数据结构位于 runtime 包内,关键结构体包括:
- runtimeTimer:定时器的底层数据结构,描述了每个定时任务的属性。
- when: 任务应当触发的时间点(以纳秒为单位)。
- period: 如果是周期性任务,表示间隔时间;否则为零。
- f: 定时器触发时的回调函数。
- arg: 传递给回调函数的参数。
- status: 表示定时器的当前状态(激活、停止等)。
- timerHeap:小顶堆数据结构,用来管理所有定时器。堆按 when 排序,最早触发的定时器在堆顶。
- time.Timer 和 time.Ticker
- time.Timer:用于管理一次性定时任务,在指定时间点触发任务并执行回调。
- time.Ticker:用于管理周期性定时任务,定期触发任务并执行回调。
两者的底层实现非常类似,都是基于 runtimeTimer,区别在于 Ticker 在每次触发任务后会重新计算下一次触发时间并继续使用。
sleep 的实现
我们通常使用 time.Sleep(1 * time.Second) 来将 goroutine 暂时休眠一段时间。sleep 操作在底层实现也是基于 timer 实现的。代码在 runtime/time.go#L84。有一些比较有意思的地方,单独拿出来讲下。 我们固然也可以这么做来实现 goroutine 的休眠:
timer := time.NewTimer(1 * time.Seconds)
<-timer.C
但 golang 底层显然不是这么做的,因为这样有两个明显的额外性能损耗。 每次调用 sleep 的时候,都要创建一个 timer 对象。 需要一个 channel 来传递事件。
既然都可以放在 runtime 里面做。golang 里面做的更加干净:
- 每个 goroutine 底层的 G 对象上,都有一个 timer 属性,这是个 runtimeTimer 对象,专门给 sleep 使用。当第一次调用 sleep 的时候,会创建这个 runtimeTimer,之后 sleep 的时候会一直复用这个 timer 对象。
- 调用 sleep 时候,触发 timer 后,直接调用 gopark,将当前 goroutine 挂起。
- timerproc 调用 callback 的时候,不是像 timer 和 ticker 那样使用 sendTime 函数,而是直接调 goready 唤醒被挂起的 goroutine。
- 这个做法和libco的poll实现几乎一样:sleep时切走协程,时间到了就唤醒协程。
func timeSleep(ns int64) {
if ns <= 0 {
return
}
gp := getg()
t := gp.timer
if t == nil {
t = new(timer)
gp.timer = t
}
t.f = goroutineReady
t.arg = gp
t.nextwhen = nanotime() + ns
if t.nextwhen < 0 { // check for overflow.
t.nextwhen = maxWhen
}
gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceBlockSleep, 1)
}