微信搜索【 腦子進煎魚了】關注這一隻爆肝煎魚。本文 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 催更。