源码分析go time.timer和ticker的stop问题

前言:

最近在优化golang的定时器时间轮,为了其他同事方便使用该库,我把golang标准库中的sleep, after, timer, ticker在时间轮里实现了一遍。但中间遇到了无法stop的问题,该问题引起了我对go定时器的研究。

问题:

比如,在时间轮里实例化了一个timer定时器对象,然后我在另一个协程里调用timer.Stop()来关闭该定时器,但 <- timer.C不会被通知到,不会被通知就一直被阻塞。

为什么被阻塞,因为我的时间轮代码里只是对定时任务的删除,而没有去close channel,timer.C自然就阻塞。我这边肯定不能直接粗暴的去close channel,因为这样有概率触发panic send on closed channel的问题。

让我们先来看下golang标准库里timer、ticker关于stop方法的实现。src/time/sleep.go里的timer stop调用的是stopTimer方法,最终的stopTimer方法在runtime/time.go里,该方法是使用go:linkname来做的映射。最后调用的是deltimer,该方法的逻辑很简单,就是在heap里删除对应的定时任务,这就完事了….

// xiaorui.cc

func (t *Timer) Stop() bool {
    if t.r.f == nil {
        panic("time: Stop called on uninitialized Timer")
    }
    return stopTimer(&t.r)
}

//go:linkname stopTimer time.stopTimer
func stopTimer(t *timer) bool {
    return deltimer(t)
}

// Delete timer t from the heap.
// Do not need to update the timerproc: if it wakes up early, no big deal.
func deltimer(t *timer) bool {
    if t.tb == nil {
        return false
    }

    tb := t.tb

    lock(&tb.lock)
    removed, ok := tb.deltimerLocked(t)
    unlock(&tb.lock)
    if !ok {
        badTimer()
    }
    return removed
}


搜寻了半天,在src/time/sleep.go, src/runtime/time.go里没找到关闭channel的逻辑。

// xiaorui.cc

// The Timer type represents a single event.
// When the Timer expires, the current time will be sent on C,
// unless the Timer was created by AfterFunc.
// A Timer must be created with NewTimer or AfterFunc.
type Timer struct {
    C <-chan Time
    r runtimeTimer
}


😅 最后不经意间,在Stop()里找到有关close channel的问题说明。

Stop prevents the Timer from firing. It returns true if the call stops the timer, false if the timer has already expired or been stopped. Stop does not close the channel, to prevent a read from the channel succeeding incorrectly.

为什么不去close channel?

我们在创建timer的时候会构建runtimeTimer对象,里面有sendTime回调方法及初始化的channel。timerproc是golang runtime的定时扫描器,当发现有任务到期后,进行相应的方法回调。但如果我们在stop里把channel给关闭了,那么timerproc有可能就panic了。

当然这问题对于go team来说不是问题… 是可以规避掉的…

// xiaorui.cc

// timer定时器的定义
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1) // buf为1, 主要为了优化timerproc的回调性能
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),  // 时间
            f:    sendTime, // 回调方法
            arg:  c,        // 参数
        },
    }
    startTimer(&t.r)
    return t
}

// 回调方法, default用来负责send.
func sendTime(c interface{}, seq uintptr) {
    select {
    case c.(chan Time) <- Now():
    default:
    }
}

// runtime/time.go
func timerproc(tb *timersBucket) {
    ...
    f := t.f
    arg := t.arg
    seq := t.seq
    unlock(&tb.lock)
    if !ok {
        badTimer()
    }
    ...
    f(arg, seq)  // 有可能会触发 panic send onclosed channel ...
    lock(&tb.lock)
    ...
}


解决方法:

既然他不去close channel,那么我们可以通过context或者创建一个stop channel来做事件的通知。参考代码如下。

// xiaorui.cc

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctimer()
    fmt.Printf("exit")
    time.Sleep(10 * time.Second)
}

func ctimer() {
    timer := time.NewTimer(5 * time.Second)
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(2 * time.Second)
        timer.Stop()
        cancel()
        fmt.Println("stop")
    }()

    for {
        select {
        case <-timer.C:
            fmt.Println("ticker.C call")
            return

        case <-ctx.Done():
            return
        }
    }
}


有人在社区里问我,在go时间轮里是怎么解决该问题的,方法依旧,都在timer和ticker结构体里加了一个context来做控制,代码已经扔到github里了,有兴趣的可以看下代码。 https://github.com/rfyiamcool/golib/blob/master/timewheel/tw.go

总结:

怎么说呢,标准库的定时器的stop方法是坑么? 不算,只能怪自己没有细心看文档注释了。😁


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc