goroutine泄露:原理、场景、检测和防范

栏目: Go · 发布时间: 6年前

内容简介:如果你启动了一个 goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是 goroutine 泄露。当 goroutine 泄露发生时,该 goroutine 的栈(一般 2k 内存空间起)一直被占用不能释放,goroutine 里的函数在堆上申请的空间也不能被 垃圾回收器 回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃。回顾一下 goroutine 终止的场景:那么当这三者同时没发生的时候,就会导致 goroutine 始终不

如果你启动了一个 goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是 goroutine 泄露。当 goroutine 泄露发生时,该 goroutine 的栈(一般 2k 内存空间起)一直被占用不能释放,goroutine 里的函数在堆上申请的空间也不能被 垃圾回收器 回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃。

回顾一下 goroutine 终止的场景:

  • 当一个goroutine完成它的工作
  • 由于发生了没有处理的错误
  • 有其他的协程告诉它终止

那么当这三者同时没发生的时候,就会导致 goroutine 始终不会终止退出。

goroutine 泄露的场景

goroutine泄露一般是因为channel操作阻塞而导致整个routine一直阻塞等待或者 goroutine 里有死循环的时候。可以细分为下面五种情况:

1. 从 channel 里读,但是没有写

// leak 是一个有 bug 程序。它启动了一个 goroutine 阻塞接收 channel。当 Goroutine 正在等待时,leak 函数会结束返回。此时,程序的其他任何部分都不能通过 channel 发送数据,那个 channel 永远不会关闭,fmt.Println 调用永远不会发生, 那个 goroutine 会被永远锁死

func leak() {
     ch := make(chan int)

     go func() {
        val := <-ch
        fmt.Println("We received a value:", val)
    }()
}

2. 向 unbuffered channel 写,但是没有读

// 一个复杂一点的例子
func sendMsg(msg, addr string) error {
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        return err
    }
    defer conn.Close()
    _, err = fmt.Fprint(conn, msg)
    return err
} 

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error)
    for _, addr := range addrs {
        go func(addr string) {
            errc <- sendMsg(msg, addr)
            fmt.Println("done")
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

func main() {
    addr := []string{"localhost:8080", "http://google.com"}
    err := broadcastMsg("hi", addr)

    time.Sleep(time.Second)

    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("everything went fine")
}

对于 broadcastMsg 里的这一段

for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }

当遇到 第一条不为 nil 的 err,broadcastMsg就返回了,那么从第二个调用 sendMsg 后返回值 err 不为 nil 的 goroutine 在 errc <- sendMsg(msg, addr) 这里都将阻塞而造成这些 goroutine 不能退出。

3. 向已满的 buffered channel 写,但是没有读

和第二种情况比较类似。

在 channel 的接收值数量有限,且可以用 buffered channel 的情况下,那 buffer size 就分配的和 接收值数量 一样,这样可以解决掉第2、3种原因造成的泄露。比如在第二种中,改成

errc := make(chan error, len(addrs))

问题就解决了。

注意:time package里的定时器使用不当也会造成 goroutine 泄露。

tick := time.Tick(1 * time.Second)
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        select {
        case <-tick:
            // Do nothing.
        case <-abort:
            fmt.Println("aborted!")
            return
        }
    }

以上的代码中,当 for 循环结束后,tick 将不再有接收者,time.Tick 启动的 goroutine 将产生泄露。

建议在程序的整个生命周期需要 ticks 时才使用 time.Tick,否则建议按如下模式使用:

ticker := time.NewTicker(1 * time.Second)
    <- ticker.C 
    ticker.Stop() // 当不再使用后,结束 ticker 的 goroutine

4. select操作在所有case上阻塞

实现一个 fibonacci 数列生成器,并在独立的 goroutine 中运行,在读取完需要长度的数列后,如果 用于 退出生成器的 quit 忘了被 close (或写入数据),select 将一直被阻塞造成 该 goroutine 泄露。

func fibonacci(c, quit chan int)  {
    x, y := 0, 1
    for{
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)

    go fibonacci(c, quit)

    for i := 0; i < 10; i++{
        fmt.Println(<- c)
    }
    
  // close(quit)
}

在这种需要一个独立的 goroutine 作为生成器的场景下,为了能在外部结束这个 goroutine,我们通常有两种方法:

  • 使用上述实现里的模式,传入一个 quit channel,配合 select,当不需要的时候,close 这个 quit channel,该 goroutine 就可以退出。
  • 使用 context 包:
func fibonacci(c chan int, ctx context.Context)  {
    x, y := 0, 1
    for{
        select {
        case c <- x:
            x, y = y, x+y
        case <-ctx.Done():
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    c := make(chan int)

    go fibonacci(c, ctx)

    for i := 0; i < 10; i++{
        fmt.Println(<- c)
    }

    cancel()

    time.Sleep(5 * time.Second)
}

5. goroutine进入死循环中,导致资源一直无法释放

通常由于代码里循环的退出条件实现的不对,导致死循环。

// 粗暴的示例
func foo() {
  for{
    fmt.Println("fooo")
  }
}

goroutine 泄露检测和定位

  1. 监控工具:固定周期对进程的内存占用情况进行采样,数据可视化后,根据内存占用走势(持续上升),很容易发现是否发生内存泄露。可以使用云服务提供的内存使用监控服务或者自己实现一个 daemon 脚本周期采集内存占用数据。
  2. 使用 Go 提供的pprof工具分析是否发生内存泄露。使用 pprof 的 heap 能够获取程序运行时的内存信息,通过对运行的程序多次采样对比,分析出内存的使用情况。

这篇文章 有关于检测和定位更详细的描述,可以参考,本处不再累述。

goroutine 泄露的防范

  • 创建goroutine时就要想好该goroutine该如何结束
  • 使用channel时,要考虑到 channel 阻塞时协程可能的行为
  • 实现循环语句时注意循环的退出条件,避免死循环

参考:

https://segmentfault.com/a/11...

https://www.ardanlabs.com/blo...

goroutine泄露:原理、场景、检测和防范


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms and Theory of Computation Handbook

Algorithms and Theory of Computation Handbook

Mikhail J. Atallah (Editor) / CRC-Press / 1998-09-30 / USD 94.95

Book Description This comprehensive compendium of algorithms and data structures covers many theoretical issues from a practical perspective. Chapters include information on finite precision issues......一起来看看 《Algorithms and Theory of Computation Handbook》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具