Go?中?time.After?可能導(dǎo)致的內(nèi)存泄露問(wèn)題解析
一、Time 包中定時(shí)器函數(shù)
go v1.20.4
定時(shí)函數(shù):NewTicker,NewTimer 和 time.After 介紹
time 包中有 3 個(gè)比較常用的定時(shí)函數(shù):NewTicker,NewTimer 和 time.After:
- NewTimer: 表示在一段時(shí)間后才執(zhí)行,默認(rèn)情況下執(zhí)行一次。如果想再次執(zhí)行,需要調(diào)用 time.Reset() 方法,這時(shí)類(lèi)似于 NewTicker 定時(shí)器了??梢哉{(diào)用 stop 方法停止執(zhí)行。
func NewTimer(d Duration) *Timer // NewTimer 創(chuàng)建一個(gè)新的 Timer,它將至少持續(xù)時(shí)間 d 之后,在向通道中發(fā)送當(dāng)前時(shí)間 // d 表示間隔時(shí)間 type Timer struct { C <-chan Time r runtimeTimer }
重置 NewTimer 定時(shí)器的 Reset()
方法,它是定時(shí)器在持續(xù)時(shí)間 d 到期后,用這個(gè)方法重置定時(shí)器讓它再一次運(yùn)行,如果定時(shí)器被激活返回 true,如果定時(shí)器已過(guò)期或停止,在返回 false。
func (t *Timer) Reset(d Duration) bool
- 用 Reset 方法需要注意的地方:
如果程序已經(jīng)從 t.C 接收到了一個(gè)值,則已知定時(shí)器已過(guò)期且通道值已取空,可以直接調(diào)用 time.Reset 方法;
如果程序尚未從 t.C 接收到值,則要先停止定時(shí)器 t.Stop(),再?gòu)?t.C 中取出值,最后調(diào)用 time.Reset 方法。
綜合上面 2 種情況,正確使用 time.Reset 方法就是:
if !t.Stop() { <-t.C } t.Reset(d)
- Stop 方法
func (t *Timer) Stop() bool // 如果定時(shí)器已經(jīng)過(guò)期或停止,返回 false,否則返回 true
Stop 方法能夠阻止定時(shí)器觸發(fā),但是它不會(huì)關(guān)閉通道,這是為了防止從通道中錯(cuò)誤的讀取值。
為了確保調(diào)用 Stop 方法后通道為空,需要檢查 Stop 方法的返回值并把通道中的值清空,如下:
if !t.Stop() { <-t.C }
- NewTicker: 表示每隔一段時(shí)間運(yùn)行一次,可以執(zhí)行多次??梢哉{(diào)用 stop 方法停止執(zhí)行。
func NewTicker(d Duration) *Ticker
NewTicker 返回一個(gè) Ticker,這個(gè) Ticker 包含一個(gè)時(shí)間的通道,每次重置后會(huì)發(fā)送一個(gè)當(dāng)前時(shí)間到這個(gè)通道上。
d
表示每一次運(yùn)行間隔的時(shí)間。
- time.After: 表示在一段時(shí)間后執(zhí)行。其實(shí)它內(nèi)部調(diào)用的就是 time.Timer 。
func After(d Duration) <-chan Time
? 跟它還有一個(gè)相似的函數(shù) time.AfterFunc
,后面運(yùn)行的是一個(gè)函數(shù)。
NewTicker 代碼例子:
package main import ( "fmt" "time" ) func main() { ticker := time.NewTicker(time.Second) defer ticker.Stop() done := make(chan bool) go func() { time.Sleep(10 * time.Second) done <- true }() for { select { case <-done: fmt.Println("Done!") return case t := <-ticker.C: fmt.Println("Current time: ", t) } } }
二、time.After 導(dǎo)致的內(nèi)存泄露
基本用法
time.After 方法是在一段時(shí)間后返回 time.Time 類(lèi)型的 channel 消息,看下面源碼就清楚返回值類(lèi)型:
// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL156C1-L158C2 func After(d Duration) <-chan Time { return NewTimer(d).C } // https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL50C1-L53C2 type Timer struct { C <-chan Time r runtimeTimer }
從代碼可以看出它底層就是 NewTimer
實(shí)現(xiàn)。
一般可以用來(lái)實(shí)現(xiàn)超時(shí)檢測(cè):
package main import ( "fmt" "time" ) func main() { ch1 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) ch1 <- "hello" }() select { case res := <-ch1: fmt.Println(res) case <-time.After(time.Second * 1): fmt.Println("timeout") } }
有問(wèn)題代碼
上面的代碼運(yùn)行是沒(méi)有什么問(wèn)題的,不會(huì)導(dǎo)致內(nèi)存泄露。
那問(wèn)題會(huì)出在什么地方?
在有些情況下,select 需要配合 for 不斷檢測(cè)通道情況,問(wèn)題就有可能出在 for 循環(huán)這里。
修改上面的代碼,加上 for + select,為了能顯示的看出問(wèn)題,加上 pprof + http 代碼,
timeafter.go:
package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main() { fmt.Println("start...") ch1 := make(chan string, 120) go func() { // time.Sleep(time.Second * 1) i := 0 for { i++ ch1 <- fmt.Sprintf("%s %d", "hello", i) } }() go func() { // http 監(jiān)聽(tīng)8080, 開(kāi)啟 pprof if err := http.ListenAndServe(":8080", nil); err != nil { fmt.Println("listen failed") } }() for { select { case _ = <-ch1: // fmt.Println(res) case <-time.After(time.Minute * 3): fmt.Println("timeout") } } }
在終端上運(yùn)行代碼:go run timeafter.go
,
然后在開(kāi)啟另一個(gè)終端運(yùn)行:go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap
,
運(yùn)行之后它會(huì)自動(dòng)在瀏覽器上彈出 pprof 的瀏覽界面,http://localhost:8081/ui/ 。
本機(jī)運(yùn)行一段時(shí)間后比較卡,也說(shuō)明程序有問(wèn)題??梢栽谶\(yùn)行一段時(shí)間后關(guān)掉運(yùn)行的 Go 程序,避免電腦卡死。
用pprof分析問(wèn)題代碼
在瀏覽器上查看 pprof 圖,http://localhost:8081/ui/ ,
從上圖可以看出,內(nèi)存使用暴漲(不關(guān)掉程序還會(huì)繼續(xù)漲)。而且暴漲的內(nèi)存集中在 time.After 上,上面分析了 time.After 實(shí)質(zhì)調(diào)用的就是 time.NewTimer,從圖中也可以看出。它調(diào)用 time.NewTimer 不斷創(chuàng)建和申請(qǐng)內(nèi)存,何以看出這個(gè)?繼續(xù)看下面分析,
再來(lái)看看哪段代碼內(nèi)存使用最高,還是用 pprof 來(lái)查看,瀏覽 http://localhost:8081/ui/source
timeafter.go
上面調(diào)用的 Go 源碼 NewTimer,
從上圖數(shù)據(jù)分析可以看出最占用內(nèi)存的那部分代碼,src/time/sleep.go/NewTimer 里的 c 和 t 分配和申請(qǐng)內(nèi)存,最占用內(nèi)存。
如果不強(qiáng)行關(guān)閉運(yùn)行程序,這里內(nèi)存還會(huì)往上漲。
為什么會(huì)出現(xiàn)內(nèi)存一直漲呢?
在程序中加了 for 循環(huán),for 循環(huán)都會(huì)不斷調(diào)用 select,而每次調(diào)用 select,都會(huì)重新初始化一個(gè)新的定時(shí)器 Timer(調(diào)用time.After,一直調(diào)用它就會(huì)一直申請(qǐng)和創(chuàng)建內(nèi)存),這個(gè)新的定時(shí)器會(huì)增加到時(shí)間堆中等待觸發(fā),而定時(shí)器啟動(dòng)前,垃圾回收器不會(huì)回收 Timer(Go源碼注釋中有解釋),也就是說(shuō) time.After 創(chuàng)建的內(nèi)存資源需要等到定時(shí)器執(zhí)行完后才被 GC 回收,一直增加內(nèi)存 GC 卻不回收,內(nèi)存肯定會(huì)一直漲。
當(dāng)然,內(nèi)存一直漲最重要原因還是 for 循環(huán)里一直在申請(qǐng)和創(chuàng)建內(nèi)存,其它是次要 。
// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL150C1-L158C2 // After waits for the duration to elapse and then sends the current time // on the returned channel. // It is equivalent to NewTimer(d).C. // The underlying Timer is not recovered by the garbage collector // until the timer fires. If efficiency is a concern, use NewTimer // instead and call Timer.Stop if the timer is no longer needed. func After(d Duration) <-chan Time { return NewTimer(d).C } // 在經(jīng)過(guò) d 時(shí)段后,會(huì)發(fā)送值到通道上,并返回通道。 // 底層就是 NewTimer(d).C。 // 定時(shí)器Timer啟動(dòng)前不會(huì)被垃圾回收器回收,定時(shí)器執(zhí)行后才會(huì)被回收。 // 如果擔(dān)心效率問(wèn)題,可以使用 NewTimer 代替,如果不需要定時(shí)器可以調(diào)用 Timer.Stop 停止定時(shí)器。
在上面的程序中,time.After(time.Minute * 3) 設(shè)置了 3 分鐘,也就是說(shuō) 3 分鐘后才會(huì)執(zhí)行定時(shí)器任務(wù)。而這期間會(huì)不斷被 for 循環(huán)調(diào)用 time.After,導(dǎo)致它不斷創(chuàng)建和申請(qǐng)內(nèi)存,內(nèi)存就會(huì)一直往上漲。
那怎么解決循環(huán)調(diào)用的問(wèn)題?解決了,就可能解決內(nèi)存一直往上漲的問(wèn)題。
解決問(wèn)題
既然是 for 循環(huán)一直調(diào)用 time.After 導(dǎo)致內(nèi)存暴漲問(wèn)題,那不循環(huán)調(diào)用 time.After 行不行?
修改后的代碼如下:
package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main() { fmt.Println("start...") ch1 := make(chan string, 120) go func() { // time.Sleep(time.Second * 1) i := 0 for { i++ ch1 <- fmt.Sprintf("%s %d", "hello", i) } }() go func() { // http 監(jiān)聽(tīng)8080, 開(kāi)啟 pprof if err := http.ListenAndServe(":8080", nil); err != nil { fmt.Println("listen failed") } }() // time.After 放到 for 外面 timeout := time.After(time.Minute * 3) for { select { case _ = <-ch1: // fmt.Println(res) case <-timeout: fmt.Println("timeout") return } } }
在終端上運(yùn)行代碼,go run timeafter1.go
,
等待半分鐘左右,在另外一個(gè)終端上運(yùn)行 go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap
,
自動(dòng)在瀏覽器上彈出界面 http://localhost:8081/ui/ ,我這里測(cè)試,界面沒(méi)有任何數(shù)據(jù)顯示,說(shuō)明修改后的程序運(yùn)行良好。
在 Go 的源碼中 After 函數(shù)注釋說(shuō)了為了更有效率,可以使用 NewTimer ,那我們使用這個(gè)函數(shù)來(lái)改造上面的代碼,
package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main() { fmt.Println("start...") ch1 := make(chan string, 120) go func() { // time.Sleep(time.Second * 1) i := 0 for { i++ ch1 <- fmt.Sprintf("%s %d", "hello", i) } }() go func() { // http 監(jiān)聽(tīng)8080, 開(kāi)啟 pprof if err := http.ListenAndServe(":8080", nil); err != nil { fmt.Println("listen failed") } }() duration := time.Minute * 2 timer := time.NewTimer(duration) defer timer.Stop() for { timer.Reset(duration) // 這里加上 Reset() select { case _ = <-ch1: // fmt.Println(res) case <-timer.C: fmt.Println("timeout") return } } }
在上面的實(shí)現(xiàn)中,也把 NewTimer 放在循環(huán)外面,并且每次循環(huán)中都調(diào)用了 Reset
方法重置定時(shí)時(shí)間。
測(cè)試,運(yùn)行 go run timeafter1.go
,然后多次運(yùn)行 go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap
,查看 pprof,我這里測(cè)試每次數(shù)據(jù)都是空白,說(shuō)明程序正常運(yùn)行。
三、網(wǎng)上一些錯(cuò)誤分析
for循環(huán)每次select的時(shí)候,都會(huì)實(shí)例化一個(gè)一個(gè)新的定時(shí)器。該定時(shí)器在多少分鐘后,才會(huì)被激活,但是激活后已經(jīng)跟select無(wú)引用關(guān)系,被gc給清理掉。換句話說(shuō),被遺棄的time.After定時(shí)任務(wù)還是在時(shí)間堆里面,定時(shí)任務(wù)未到期之前,是不會(huì)被gc清理的
上面這種分析說(shuō)明,最主要的還是沒(méi)有說(shuō)清楚內(nèi)存暴漲的真正內(nèi)因。如果用 pprof 的 source 分析查看,就一目了然,那就是 NewTimer 里的 2 個(gè)變量創(chuàng)建和申請(qǐng)內(nèi)存導(dǎo)致的。
四、參考
- https://pkg.go.dev/time#pkg-overview
- https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go
- https://www.cnblogs.com/jiujuan/p/14588185.html pprof 基本使用
- 《100 Go Mistakes and How to Avoid Them》 作者:Teiva Harsanyi
到此這篇關(guān)于Go 中 time.After 可能導(dǎo)致的內(nèi)存泄露的文章就介紹到這了,更多相關(guān)go time.After 內(nèi)存泄露內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Go defer與time.sleep的使用與區(qū)別
- 詳解Golang time包中的結(jié)構(gòu)體time.Ticker
- 詳解Golang time包中的time.Duration類(lèi)型
- 詳解Golang time包中的結(jié)構(gòu)體time.Time
- Golang time.Sleep()用法及示例講解
- go?time.Sleep睡眠指定時(shí)間實(shí)例詳解(小時(shí)級(jí)到納秒級(jí))
- 淺談golang 中time.After釋放的問(wèn)題
- 解決Golang time.Parse和time.Format的時(shí)區(qū)問(wèn)題
- go語(yǔ)言time.After()的作用
相關(guān)文章
GoLang的sync.WaitGroup與sync.Once簡(jiǎn)單使用講解
sync.WaitGroup類(lèi)型,它比通道更加適合實(shí)現(xiàn)這種一對(duì)多的goroutine協(xié)作流程。WaitGroup是開(kāi)箱即用的,也是并發(fā)安全的。同時(shí),與之前提到的同步工具一樣,它一旦被真正的使用就不能被復(fù)制了2023-01-01Golang中json和jsoniter的區(qū)別使用示例
這篇文章主要介紹了Golang中json和jsoniter的區(qū)別使用示例,本文給大家分享兩種區(qū)別,結(jié)合示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2023-12-12Golang標(biāo)準(zhǔn)庫(kù)container/list的用法圖文詳解
提到單向鏈表,大家應(yīng)該是比較熟悉的了,這篇文章主要為大家詳細(xì)介紹了Golang標(biāo)準(zhǔn)庫(kù)container/list的用法相關(guān)知識(shí),感興趣的小伙伴可以了解下2024-01-01go語(yǔ)言題解LeetCode674最長(zhǎng)連續(xù)遞增序列
這篇文章主要為大家介紹了go語(yǔ)言題解LeetCode674最長(zhǎng)連續(xù)遞增序列示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Go語(yǔ)言實(shí)現(xiàn)基于websocket瀏覽器通知功能
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)基于websocket瀏覽器通知功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07Go語(yǔ)言并發(fā)之Sync包的6個(gè)關(guān)鍵概念總結(jié)
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言并發(fā)中Sync包的6個(gè)關(guān)鍵概念,文中的示例代碼講解詳細(xì),對(duì)我們深入學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以參考一下2023-05-05