Golang定时器要点

2022-04-07
2分钟阅读时长

golang内置的定时器是基于多个四叉堆封装调度的,增删定时器的效率是O(lgn),所以大量定时器本身可能成为性能瓶颈。可以考虑使用开源的时间轮算法实现方案进行替换。不过一般情况下不需要考虑该问题,如果时间轮真的万能的话,官方肯定就重构了。

复用定时器技巧:

for {
    t = timer.NewTimer(5 * time.Second)
    defer t.Stop()
    select {
    case b := <- a:
        fmt.Print(b)
        break
    case <-t.C:
        fmt.Print("timeout")
        continue
    }
}

这种循环场景下回创建大量定时器,对性能有较大影响,所以应该通过Reset进行复用。即:

t = timer.NewTimer()
for{
    t.Reset(5 * time.Second)
    //...
}

timer.Reset的使用很容易出现各种问题,这主要是timer.Stop实际上并不会关闭t.C这个channel,因此需要:

if !t.Stop(){
    <-t.C
}

来判断t是expired还是stop成功,如果expired,需要将t.C里面的信号取出来抛弃掉。

令人蛋疼的是,在Reset之前是否需要做这一步完全取决于t.C里面是否有数据。如果确认t.C里面的数据已经取出,就必须不能做这一步,否则会卡在<-t.C这里。

而看似万无一失的:

if !t.Stop(){
    select{
    case <-t.C:
    default:
    }
}

一样有问题。由于go的timer源码里存在竞态条件,导致t.Stop<-t.C这两部之间存在时序问题,这么干了可能会导致后面用到<-t.C的地方立刻触发。

所以官方给出的解答是:

  • 程序始终在同一个 goroutine 中进行 Timer 的 Stop、Reset 和 receive/drain channel 操作
  • 程序需要维护一个状态变量,用于记录它是否已经从 channel 中接收过事件,进而作为 Stop 中 draining 操作的判断依据

一定要小心谨慎地使用。

Avatar

个人介绍

兴趣使然的程序员,博而不精,乐学不倦