昨夜西风凋碧树
在Golang中select的四大用法/#超时控制 中,提到select搭配time.After
实现超时控制。其实这样写是有问题的。
由于这种写法每次都会初始化新的time.After,当等待时间较长,比如1分钟,会发生内存泄露(当然问题并不仅限于此,继续看下去)
不知道是谁带的,在Go中用select和time.After做超时控制,近乎于成了事实上的标准,像Golang time.After()用法及代码示例 这样的例子网上比比皆是 在许多大公司代码仓库里,一搜<- time.After
关键字有一大堆,而且后面的时间不少都是几分钟。 用pprof看下不难发现,这是教科书级的错误…每次都初始化,但执行前不会被回收,造成内存暴涨。 建议有空去搜下,看看是不是代码里这种用法有一大把…
可以参考
峰云-分析golang time.After引起内存暴增OOM问题
慎用time.After会造成内存泄漏(golang)
这几篇分析,实际验证一下
after.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main () { go func () { if err := http.ListenAndServe(":6060" , nil ); err != nil { fmt.Printf("start pprof failed on %s,err%v \n" , "6060" , err) } }() ch := make (chan string , 100 ) go func () { for { ch <- "向管道塞入数据" } }() for { select { case <-ch: case <-time.After(time.Minute * 3 ): } } }
运行这段程序,然后执行
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
这行命令可以分成三部分:
-http=:8081
是指定以web形式,在本地8081端口启动 (如果不加-http=:8081
参数,则会进入命令行交互,在命令行中再输入web
与直接使用-http=:8081
参数效果等效)
http://localhost:6060/debug/pprof/heap
是指定获取profile文件的地址。本地在实时运行的程序可以用这种方式,更多情况下(如在服务器上,没有对外开放用于pprof的端口),可以先去机器上,用curl http://127.0.0.1:6060/debug/pprof/heap -o heap_cui.out
拿到profile文件,再想办法弄到本地,使用go tool pprof --http :9091 heap_cui.out
进行分析
而且随着时间推移,程序占用的内存会继续增加
从调用图可发现, 程序不断调用time.After,进而导致计时器 time.NerTimer
不断创建和内存申请
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func After (d Duration) <-chan Time { return NewTimer(d).C }
在select里面虽然没有执行到time.After,但每次都会初始化,会在时间堆里面,定时任务未到期之前,是不会被gc清理的
在计时器触发之前,垃圾收集器不会回收Timer
如果考虑效率,需要使用NewTimer替代
衣带渐宽终不悔
使用NewTimer 或NewTicker替代:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main () { go func () { if err := http.ListenAndServe(":6060" , nil ); err != nil { fmt.Printf("start pprof failed on %s,err%v \n" , "6060" , err) } }() ticker := time.NewTicker(time.Minute * 3 ) ch := make (chan string , 100 ) go func () { for { ch <- "向管道塞入数据" } }() for { select { case <-ch: case <-ticker.C: print ("结束执行" ) } } }
这篇Go 内存泄露之痛,这篇把 Go timer.After 问题根因讲透了! 应该有点问题,不是内存孤儿,gc还是会去回收的,只是要在time.After到期之后
众里寻他千百度
如上是网上大多数技术文章的情况:
其实针对本例,这些说法都没有切中肯綮
最初的代码仅仅是有内存泄露的问题吗?
实际上,即便3分钟后,第2个case也得不到执行 (可以把3min改成2s验证下)
只要第一个case能不断从channel中取出数据(在此显然可以),那第二个case就永远得不到执行。这是因为每次time.After都被重新初始化了,而上面那个case一直满足条件,当然就是第二个case一直得不到执行 , 除非第一个case超过3min没有从channel中拿到数据
所以其实在此例中NewTimer还是NewTicker,都不是问题本质,这个问题本质,就是个变量作用域的问题
在for循环外定义time.After(time.Minute * 3),如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main () { go func () { if err := http.ListenAndServe(":6060" , nil ); err != nil { fmt.Printf("start pprof failed on %s,err%v \n" , "6060" , err) } }() ch := make (chan string , 100 ) go func () { for { ch <- "向管道塞入数据" } }() timeout := time.After(time.Minute * 3 ) for { select { case <-ch: case <-timeout: fmt.Println("到了这里" ) } } }
把time.After放到循环外,可以看到,并没有什么内存泄露,3min(可能多一点点)后,如期执行到了第2个case
所以在这个场景下,并不是time.After 在计时器触发之前,垃圾收集器不会回收Timer 的问题,而是最起码的最被忽略的变量作用域问题..
(程序员的锅,并不是time.After的问题…用NewTimer还是NewTicker之所以不会内存泄露,只是因为是在for循环外面初始化的…)
之前在for循环里case <-time.After(time.Minute * 3)
的写法,效果类似下面:
1 2 3 4 5 6 7 8 9 10 11 package mainimport "time" func main () { for { time.After(2 * time.Second) } }
验证是否会成为所谓的”内存孤儿”
改造程序,验证一下:
在计时器触发之前,垃圾收集器不会回收Timer ;
但在计时器触发后,垃圾收集器会回收这些Timer ,并不会造成“内存孤儿”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package mainimport ( "fmt" "net/http" _ "net/http/pprof" "sync/atomic" "time" ) func main () { go func () { if err := http.ListenAndServe(":6060" , nil ); err != nil { fmt.Printf("start pprof failed on %s,err%v \n" , "6060" , err) } }() after() fmt.Println("程序结束" ) } func after () { var i int32 ch := make (chan string , 0 ) done := make (chan string ) go func () { for { select { default : atomic.AddInt32(&i, 1 ) ch <- fmt.Sprintf("%s%d%s" , "向管道第" , i, "次塞入数据" ) case exit := <-done: fmt.Println("关闭通道" , exit) return } } }() go func () { time.Sleep(time.Second) done <- "去给我通知不要再往ch这个channel里写数据了!" }() for { select { case res := <-ch: fmt.Println("res:" , res) case <-time.After(2 * time.Second): fmt.Println("结束接收通道的数据" ) return } } }
去掉打印的信息,替换为当前实时的内存信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package mainimport ( "fmt" "net/http" _ "net/http/pprof" "runtime" "sync/atomic" "time" ) func main () { go func () { if err := http.ListenAndServe(":6060" , nil ); err != nil { fmt.Printf("start pprof failed on %s,err%v \n" , "6060" , err) } }() after() fmt.Println("程序结束" ) } func after () { var ms runtime.MemStats runtime.ReadMemStats(&ms) fmt.Println("before, have" , runtime.NumGoroutine(), "goroutines," , ms.Alloc, "bytes allocated" , ms.HeapObjects, "heap object" ) var i int32 ch := make (chan string , 0 ) done := make (chan string ) go func () { for { select { default : atomic.AddInt32(&i, 1 ) ch <- fmt.Sprintf("%s%d%s" , "向管道第" , i, "次塞入数据" ) case exit := <-done: fmt.Println("关闭通道" , exit) return } } }() go func () { time.Sleep(time.Second) done <- "去给我通知不要再往ch这个channel里写数据了!" }() for { select { case res := <-ch: runtime.GC() runtime.ReadMemStats(&ms) fmt.Printf("%s,now have %d goroutines,%d bytes allocated, %d heap object \n" , res, runtime.NumGoroutine(), ms.Alloc, ms.HeapObjects) case <-time.After(2 * time.Second): runtime.GC() fmt.Println("当前结束接收通道的数据,准备返程" ) runtime.ReadMemStats(&ms) fmt.Printf("now have %d goroutines,%d bytes allocated, %d heap object \n" , runtime.NumGoroutine(), ms.Alloc, ms.HeapObjects) return } } }
更多参考:
Go time.NewTicker()与定时器
tech-talk-time.After不断初始化的
↑ BTC Address:3NNxkM6ez7szsUAgTnK2VaF949LoGmXuBs