Go使用time.Stop()不当导致的问题

ivansli 2021/11/19 376℃ 0

问题

先看一段代码

func main() {
    wg := &sync.WaitGroup{}
    timer := time.NewTimer(5 * time.Second)

    wg.Add(1)
    gofunc() {
        defer wg.Done()
        <-timer.C

        fmt.Println("timer expired, goroutine exists")
    }()

    time.Sleep(1 * time.Second)
    fmt.Println("Sleep Second")

    timer.Stop()
    fmt.Println("timer stop")

    wg.Wait()
}

聪明的你觉得执行结果会是什么?
A. 正常结束
B. 永远无法结束

代码分析

一般来说,拿出来特别说明的例子基本都会存在一些问题。上面代码的结果是B(如果选择A的话,就要好好思考一下了)

这里重点说明两处代码

// 创建timer对象,并插入时间堆中,如果时间达到触发时刻,会向timer.C中发送数据
// 其中 timer.C 为 make(chan Time, 1),为什么缓冲为1,可以思考一下
timer := time.NewTimer(5 * time.Second)

// 把timer对象从时间堆中移除
// 此方法有两种返回结果
// 1.已经过了触发事件或者已经被停止,则返回false
// 2.移除成功返回true
timer.Stop()

上面的代码中,调用timer.Stop() 时,还未到达触发事件,所以会返回true。那么问题来了,从时间堆中移除该timer对象的话,timer.C是怎么处理的,会关闭吗?

结论是:不会关闭。
造成的问题:那么goroutine中<-timer.C 就会永久阻塞,则main函数也会因此永久阻塞。

正确的写法

针对上面有问题的代码,正确的写法是:

func main() {
    wg := &sync.WaitGroup{}
    timer := time.NewTimer(5 * time.Second)

    wg.Add(1)
    gofunc() {
        defer wg.Done()
        <-timer.C

        fmt.Println("timer expired, goroutine exists")
    }()

    time.Sleep(1 * time.Second)
    fmt.Println("Sleep Second")

    //timer.Stop()
    timer.Reset(0 * time.Second)
    fmt.Println("timer stop")

    wg.Wait()

    fmt.Println("main exists")
}

timer.Stop() 改为 timer.Reset(0 * time.Second) 意味着重置timer,由于设置的超时时间为 0,则立即触发,会向timer.C发送数据。这个时候,goroutine中<-timer.C 立刻能够获得数据。

结论

这里我们要思考以下几点:

  1. 牵扯channel使用时,要先思考怎样才能不会阻塞在channel上
  2. 使用goroutine时,要先思考怎样才能正常退出
  3. 使用包时,需要大概了解一下内部原理

golang中泄露问题,大部分无外乎上面几种情况。只有掌握了原理,才能减少bug,事半功倍。

Golang

评论啦~