今天看到了一篇有关 timer 泄露的文章,觉得很有意思,于是把它记录下来。
一般没有问题的写法
说道 time.After() 会导致内存泄露,很多人一定会觉得奇怪,因为代码里经常会用到它,也没见有内存泄漏啊?
是的,一般我们这样写的话是没有问题的
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
select {
case _ = <-ch:
case <-time.After(time.Second * 1):
fmt.Println("timeout")
}
}
有问题的写法
那么,什么样的写法有问题呢? 当使用 for
loop 的时候,比如这样
for {
select {
case _ = <-ch:
// do something...
continue
case <-time.After(300 * time.Millisecond):
fmt.Printf("time.After() fire!\n")
}
}
很不幸的是,上面这样的写法也非常常见,我自己就写过这样的代码。那么它真的会造成内存泄露吗?试一下便知道
前一篇博客中已经介绍了如何使用 pprof 对 Go 程序进行 profiling,简单提一下步骤
- 在代码中引入
_ "net/http/pprof"
, 并开启一个http server 导出 metrics - 运行你的 binary
- 执行
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
- 浏览器就会自动打开
localhost:8081
显示结果了
测试代码如下:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
go func() {
ip := "127.0.0.1:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
}
}()
ch := make(chan int, 10)
go func() {
in := 1
for {
in++
ch <- in
}
}()
for {
select {
case _ = <-ch:
// do something...
continue
case <-time.After(3 * time.Minute):
fmt.Printf("time.After() fire!\n")
}
}
}
使用 pprof 后,可见 timer 内存占用居然超过了 1GB !!
同时,从操作系统里也能看到 binary 运行时和运行后,内存的大幅变化。
原因
那么是什么原因导致的呢? 通过参考资料的解释,是 select 语句每次执行都是创建了新的 timer,当有 for 循环时,每次都在不断地创建 timer,而 timer 直到超时以后才会被 GC 回收,否则就算当前的作用域执行完了,或者 goroutine 退出,timer依然存在内存。
Todo: 如果没看到这篇文章,我怎么知道 select 语句每次都是新建 timer呢? 似乎应该有能力从 Go 编译源码的步骤,或者生成的汇编代码看出来,否则永远都是别人告诉你有坑你才知道。
优化
上面的例子故意把 timeout 设置为 3分钟,同时有不断的 <- ch
输入,所以会有大量的 timer 没有被 GC 回收,如果把 timeout 的时间设置小一点呢?
还是同样的代码,如果把 timeout 设置为 300ms,内存占用如下
大概在 200MB 左右,可见,减小 timeout 时间,在一定程度上能减少内存泄露的量。但是这个还是治标不治本的方法。
解决办法
一个解决办法是不在 select 內部创建 timer,把它放到外部,并每次 reset timer
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
go func() {
ip := "127.0.0.1:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
}
}()
ch := make(chan int, 10)
go func() {
in := 1
for {
in++
ch <- in
}
}()
dur := 300 * time.Millisecond
myTimer := time.NewTimer(dur)
defer myTimer.Stop()
for {
myTimer.Reset(dur)
select {
case _ = <-ch:
// do something...
continue
case <-myTimer.C:
fmt.Printf("timer fired\n")
}
}
}
但是,当 select 语句在一个单独的 goroutine 里面,外部是在 for loop 中调用这个 goroutine,似乎就没有很好的办法了,除非把 timer 作为参数传入。