talkgo / night

Weekly Go Online Meetup via Bilibili|Go 夜读|通过 bilibili 在线直播的方式分享 Go 相关的技术话题,每天大家在微信/telegram/Slack 上及时沟通交流编程技术话题。
https://talkgo.org
MIT License
11.98k stars 1.16k forks source link

【分享】说说 Go 中的那些定时器 timer #371

Closed yangwenmai closed 5 years ago

yangwenmai commented 5 years ago

参考资料

  1. ~从99.9%CPU浅谈Golang的定时器实现原理~
  2. Golang 定时器实现
  3. Golang 定时器陷进
  4. Go 系列文章5:定时器
MaruHyl commented 5 years ago

第一个链接有部分内容的理解是错误的

其次golang的处理方式中也可以看出,
go的timer的处理和用户端程序定义的间隔时间不一定完全精准,
用户的回调函数执行时间越长单个timer对堆中其他邻近timer的影响越大。
因此timer的回调函数一定是执行时间越短越好。

事实上标准库的封装考虑到了这个问题,所以内置的回调函数只有这两个

// ...
func sendTime(c interface{}, seq uintptr) {
    // Non-blocking send of time on c.
    // Used in NewTimer, it cannot block anyway (buffer).
    // Used in NewTicker, dropping sends on the floor is
    // the desired behavior when the reader gets behind,
    // because the sends are periodic.
    select {
    case c.(chan Time) <- Now():
    default:
    }
}
// ...
func goFunc(arg interface{}, seq uintptr) {
    go arg.(func())()
}

换句话说,都是非阻塞的,而用户的自定义回调函数,是作为arg传进去然后分配新的g去执行的,所以不存在下面说的这个问题

//...
用户的回调函数执行时间越长单个timer对堆中其他邻近timer的影响越大。
因此timer的回调函数一定是执行时间越短越好。
Shitaibin commented 5 years ago

第3个文章是我写的,有问题请指出

MaruHyl commented 5 years ago

@Shitaibin 第三篇文章写的挺好的,就是篇幅有点长了,而且StopReset的api文档已经写了,所以应该不算陷阱把。。然后我觉得这种写法会更好

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

主要区别就是这里会对t.C加锁,但len(t.C)不会对t.C加锁. 不过严格来说的话,这些逻辑都在timer.Lock外,所以没有执行顺序保证,那么完全有可能存在这样一条path, 1.timeproc发现timer过期,删除timer,然后解锁,准备执行sendtime 2.用户执行t.Stop,对timerbucket加锁,发现timer已经被删除,所以返回false 3.用户消费t.C失败,因为timerproc还没有执行sendtime 4.timerproc执行sendtime成功 这种概率很低,但是确实是个问题,而且我也没想到更好的写法。

acynothia commented 5 years ago

如果想要复用 timer 重复设置超时,可以参考 http://github.com/google/netstack/tcpip/transport/tcp/timer.go 重复使用时保存新的 target.

Shitaibin commented 5 years ago

@MaruHyl 我没有明白你说的这种方式更好在哪,Timer.Stop()会停止继续向通道中写数据,就算超时也不会再写数据,也就是没有goroutine继续向Timer.C写数据了,这时调用者len(Timer.C),本质上只有1个goroutine访问Timer.C的数据了,我认为和Timer.C是否加锁无关。

另外,timer.Lock指啥,Timer和runtimeTimer都没看到这个Lock字段。

Shitaibin commented 5 years ago

@Astone-Chou 这个思路很6,有一个地方没看懂,请教下:

enable函数是开始复用1个timer:

// enable enables the timer, programming the runtime timer if necessary.
func (t *timer) enable(d time.Duration) {
    t.target = time.Now().Add(d)

    // Check if we need to set the runtime timer.
    if t.state == timerStateDisabled || t.target.Before(t.runtimeTarget) {
        t.runtimeTarget = t.target
        t.timer.Reset(d)
    }

    t.state = timerStateEnabled
}

调用的地方有这种情况:没有检查timer是否disable()就直接调用enable:

image

那是否就需要在调用者的代码逻辑上,确保在enable()时,之前设置的timer在已经不用了?

acynothia commented 5 years ago

重复 enable 实际上就是你上面说的 reset 一个已经启动的定时器。同时修改 target。再 resume 的时候检测一下是不是最新 enable 的定时器(如果是老的定时器触发的就重新Reset)

MaruHyl commented 5 years ago

@Shitaibin 会的,可能会存在两个goroutine在访问timer.C,timeproc和消费段。 timer.Lock指的是https://github.com/golang/go/blob/master/src/runtime/time.go#L64。

@Astone-Chou 你发的那个适用场景太特殊了,有点偏离我指的复用timer的主题(比如timer pool,完全独立的goroutines复用timer,上一次putback的timer不会影响下一次复用)。 然而你给的tcp-timer并不是,它为了减少runtime call的次数,disable只是重置了timerState,甚至由于timerStateOrphaned状态的存在,重新enable的时候可能导致timer过早的唤醒(如果timerStateOrphanednewTarget > oldTargettimer不会被Reset,同样也是为了减少runtime call),但是这些trick都是针对于它的场景做的优化。 如果是一个timer pool的场景,你putback的时候就必须要真正的Stop掉timer,并且考虑如何保证清空掉timer.C才不会影响到下一次的使用。

Shitaibin commented 5 years ago

@MaruHyl 你是对的,存在你说的这种路径,这就导致了最后t.C里还有个数据