教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 goroutine泄露:原理、场景、检测和防范

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

发布时间:2021-12-25   编辑:jiaochengji.com
教程集为您提供goroutine泄露:原理、场景、检测和防范等资源,欢迎您收藏本站,我们将为您提供最新的goroutine泄露:原理、场景、检测和防范资源

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

回顾一下 goroutine 终止的场景:

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

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

<h3>goroutine 泄露的场景</h3>

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

<h4>1. 从 channel 里读,但是没有写</h4> <pre><code class="lang-go hljs">// 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) }() }</code></code></pre> <h4>2. 向 unbuffered channel 写,但是没有读</h4> <pre><code class="lang-go hljs">// 一个复杂一点的例子 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") }</code></code></pre>

对于 broadcastMsg 里的这一段

<pre><code class="lang-go hljs">for _ = range addrs { if err := <-errc; err != nil { return err } }</code></code></pre>

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

<h4>3. 向已满的 buffered channel 写,但是没有读</h4>

和第二种情况比较类似。

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

<pre><code class="lang-go hljs">errc := make(chan error, len(addrs))</code></code></pre>

问题就解决了。

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

<pre><code class="lang-go hljs"> 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 } }</code></code></pre>

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

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

<pre><code class="lang-go hljs"> ticker := time.NewTicker(1 * time.Second) <- ticker.C ticker.Stop() // 当不再使用后,结束 ticker 的 goroutine</code></code></pre> <h4>4. select操作在所有case上阻塞</h4>

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

<pre><code class="lang-go hljs">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) }</code></code></pre>

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

<ul><li>使用上述实现里的模式,传入一个 quit channel,配合 select,当不需要的时候,close 这个 quit channel,该 goroutine 就可以退出。</li> <li>使用 context 包:</li> </ul><pre><code class="lang-go hljs">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) }</code></code></pre> <h4>5. goroutine进入死循环中,导致资源一直无法释放</h4>

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

<pre><code class="lang-go hljs">// 粗暴的示例 func foo() { for{ fmt.Println("fooo") } }</code></code></pre> <h3>goroutine 泄露检测和定位</h3> <ol><li>监控工具:固定周期对进程的内存占用情况进行采样,数据可视化后,根据内存占用走势(持续上升),很容易发现是否发生内存泄露。可以使用云服务提供的内存使用监控服务或者自己实现一个 daemon 脚本周期采集内存占用数据。</li> <li>使用Go提供的pprof工具分析是否发生内存泄露。使用 pprof 的 heap 能够获取程序运行时的内存信息,通过对运行的程序多次采样对比,分析出内存的使用情况。</li> </ol>

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

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

参考:

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

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

<span class="img-wrap"></span>


到此这篇关于“goroutine泄露:原理、场景、检测和防范”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
goroutine泄露:原理、场景、检测和防范
golang 切片截取 内存泄露_怎么看待Goroutine 泄露
Goroutine并发调度模型深度解析&amp;手撸一个协程池
字节跳动的 Go 语言面试会问哪些问题?
Go 笔记之如何防止 goroutine 泄露
golang struct 字段null_给大家丢脸了,用了三年golang,我还是没答对这道内存泄漏题。...
定位分析内存泄漏的原因和后果
golang 面试题(从基础到高级)
golang goroutine 通知_深入golang之---goroutine并发控制与通信
golang bufio.newscanner如何超时跳出_贝壳找房小程序从PHP到Golang的跃迁之路

[关闭]
~ ~