微信搜索【 脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blog 已收录,有个人系列文章、资料和开源 Go 图书。
你们好,我是煎鱼。git
前几天分享了一篇 Go timer 源码解析的文章《难以驾驭的 Go timer,一文带你参透计时器的奥秘》。github
在评论区有小伙伴提到了经典的 timer.After
泄露问题,但愿我能聊聊,这是一个不能不知的一个大 “坑”。golang
今天这篇文章煎鱼就带你们来研讨一下这个问题。面试
今天是男主角是Go 标准库 time 所提供的 After
方法。函数签名以下:算法
func After(d Duration) <-chan Time
该方法能够在必定时间(根据所传入的 Duration)后主动返回 time.Time
类型的 channel 消息。编程
在常见的场景下,咱们会基于此方法作一些计时器相关的功能开发,例子以下:微信
func main() { ch := make(chan string) go func() { time.Sleep(time.Second * 3) ch <- "脑子进煎鱼了" }() select { case _ = <-ch: case <-time.After(time.Second * 1): fmt.Println("煎鱼出去了,超时了!!!") } }
在运行 1 秒钟后,输出结果:函数
煎鱼出去了,超时了!!!
上述程序在在运行 1 秒钟后将触发 time.After
方法的定时消息返回,输出了超时的结果。工具
从例子来看彷佛很是正常,也没什么 “坑” 的样子。难道是 timer.After
方法的虚晃一枪?性能
咱们再看一个不像是有问题例子,这在 Go 工程中常常能看见,只是你们都没怎么关注。
代码以下:
func main() { 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("如今是:%d,我脑子进煎鱼了!", time.Now().Unix()) } } }
在上述代码中,咱们构造了一个 for+select+channel
的一个经典的处理模式。
同时在 select+case
中调用了 time.After
方法作超时控制,避免在 channel
等待时阻塞太久,引起其余问题。
看上去都没什么问题,可是细心一看。在运行了一段时间后,粗暴的利用 top
命令一看:
个人 Go 工程的内存占用居然已经达到了 10+GB 之高,而且还在持续增加,很是可怕。
在所设置的超时时间到达后,Go 工程的内存占用彷佛一时半会也没有要回退下去的样子,这,到底发生了什么事?
抱着一脸懵逼的煎鱼,我默默的掏出我早已埋好的 PProf,这是 Go 语言中最强的性能分析剖析工具,在我出版的 《Go 语言编程之旅》特地有花量章节的篇幅大面积将讲解过。
在 Go 语言中,PProf 是用于可视化和分析性能分析数据的工具,PProf 以 profile.proto 读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)。
咱们直接用 go tool pprof
分析 Go 工程中函数内存申请状况,以下图:
从图来分析,能够发现是不断地在调用 time.After
,从而致使计时器 time.NerTimer
的不断建立和内存申请。
这就很是奇怪了,由于咱们的 Go 工程里只有几行代码与 time
相关联:
func main() { ... for { select { ... case <-time.After(3 * time.Minute): fmt.Printf("如今是:%d,我脑子进煎鱼了!", time.Now().Unix()) } } }
因为 Demo 足够的小,咱们相信这就是问题代码,但缘由是什么呢?
缘由在于 for
+select
,再加上 time.After
的组合会致使内存泄露。由于 for
在循环时,就会调用都 select
语句,所以在每次进行 select
时,都会从新初始化一个全新的计时器(Timer)。
咱们这个计时器,是在 3 分钟后才会被触发去执行某些事,但重点在于计时器激活后,却又发现和 select
之间没有引用关系了,所以很合理的也就被 GC 给清理掉了,由于没有人须要 “我” 了。
要命的还在后头,被抛弃的 time.After
的定时任务仍是在时间堆中等待触发,在定时任务未到期以前,是不会被 GC 清除的。
但很惋惜,他 “永远” 不会到期了,也就是为何咱们的 Go 工程内存会不断飙高,实际上是 time.After
产生的内存孤儿们致使了泄露。
既然咱们知道了问题的根因代码是不断的重复建立 time.After
,又无法完整的走完释放的闭环,那解决办法也就有了。
改进后的代码以下:
func main() { timer := time.NewTimer(3 * time.Minute) defer timer.Stop() ... for { select { ... case <-timer.C: fmt.Printf("如今是:%d,我脑子进煎鱼了!", time.Now().Unix()) } } }
通过一段时间的摸鱼后,再使用 PProf 进行采集和查看:
Go 进程的各项指标正常,无缺的解决了这个内存泄露的问题。
在今天这篇文章中,咱们介绍了标准库 time
的基本常规使用,同时针对 Go 小伙伴所提出的 time.After
方法的使用不当,所致使的内存泄露进行了重现和问题解析。
其根因就在于 Go 语言时间堆的处理机制和常规 for
+select
+time.After
组合的下意识写法所致使的泄露。
忽然想起我有一个朋友在公司里有看到过相似的代码,在生产踩过这个坑,半夜被告警抓起来...
不知道你在平常工做中有没有遇到过类似的问题呢,欢迎留言区评论和交流。
文章持续更新,能够微信搜【脑子进煎鱼了】阅读,回复【 000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blog 已收录,欢迎 Star 催更。