Go1.23 新特性:花了近 10 年,time.After 終于不泄漏了!
大家好,我是煎魚。
好多年前,我寫過 timer.After 的使用和坑。Go 這么多年以來這塊一直有內(nèi)存泄露。有的同學(xué)或多或少都有遇到過。
最近 Go1.23 即將正式發(fā)布,Go 核心團(tuán)隊負(fù)責(zé)人 rsc 自述花了將近 10 年的努力,終于把這個問題修復(fù)了。值得我們關(guān)注!
timer.After 是什么
這是之前編寫的部分,我測試驗證了下。在 Go1.22 依然有效,仍然是有問題的。因此沒有做什么修改。主要是給大家做知識溫習(xí)回顧的作用。
今天是男主角是 Go 標(biāo)準(zhǔn)庫 time 所提供的 After 方法。函數(shù)簽名如下:
func After(d Duration) <-chan Time
該方法可以在一定時間(根據(jù)所傳入的 Duration)后主動返回 time.Time 類型的 channel 消息。
在常見的場景下,我們會基于此方法做一些計時器相關(guān)的功能開發(fā),例子如下:
func main() {
ch := make(chan string)
go func() {
time.Sleep(time.Second * 3)
ch <- "腦子進(jìn)煎魚了"
}()
select {
case _ = <-ch:
case <-time.After(time.Second * 1):
fmt.Println("煎魚出去了,超時了?。?!")
}
}
在運行 1 秒鐘后,輸出結(jié)果:
煎魚出去了,超時了?。?!
上述程序在在運行 1 秒鐘后將觸發(fā) time.After 方法的定時消息返回,輸出了超時的結(jié)果。
有什么問題和坑
從例子來看似乎非常正常,也沒什么 “坑” 的樣子。莫非是虛晃一槍?
我們再看一個不像是有問題例子,這在 Go 工程中經(jīng)常能看見,只是大家都沒怎么關(guān)注。
代碼如下:
func main() {
ch := make(chan int, 10)
go func() {
in := 1
for {
in++
ch <- in
}
}()
for {
select {
case _ = <-ch:
// 煎魚干了點什么...
continue
case <-time.After(3 * time.Minute):
fmt.Printf("現(xiàn)在是:%d,我腦子進(jìn)煎魚了!", time.Now().Unix())
}
}
}
在上述代碼中,我們構(gòu)造了一個 for+select+channel 的一個經(jīng)典的處理模式。
同時在 select+case 中調(diào)用了 time.After 方法做超時控制,避免在 channel 等待時阻塞過久,引發(fā)其他問題。
看上去都沒什么問題,但是細(xì)心一看。在運行了一段時間后,我的筆記本電腦已經(jīng)溫?zé)崃嗽S多。
粗暴的利用 top 命令一看:
圖片
例子中 Go 工程的內(nèi)存占用竟然已經(jīng)達(dá)到了 30+GB 之高,并且還在持續(xù)增長。在再等待了一段時間后(所設(shè)置的超時時間到達(dá)),Go 工程的內(nèi)存占用也沒有要恢復(fù)合理的數(shù)值。這非??膳?。
這明顯就是存在內(nèi)存泄露的問題。
問題原因
這個內(nèi)存泄露的問題,無容置疑是 Go 官方認(rèn)可的 BUG。
快速的用一句話來講,核心原因在于:for select 已結(jié)束,無法被 GC,時間堆內(nèi)的被觸發(fā)的計時器還在。
Go 官方文檔說明
如果是想深入看原因可以查看以前我寫的《Go 內(nèi)存泄露之痛,這篇把 Go timer.After 問題根因講透了!》
Go1.23 timer.After 不泄露了!
在現(xiàn)在 2024 年,經(jīng)過將近十年的努力,Go 核心團(tuán)隊負(fù)責(zé)人 rsc 終于解決了這個問題?。?!
圖片
自 Go1.23 版本起,會對用于計時器的通道(或者可能是用于通道的計時器)進(jìn)行特殊處理,以便當(dāng)沒有通道操作待處理時,計時器將不會存放在計時器堆中。
這意味著當(dāng)一旦不再引用通道和計時器,就可以對其進(jìn)行 GC,不必等待計時器到期或明確停止計時器。
注:這里的計時器是指 time.After、time.NewTimer 和 time.NewTicker 使用的數(shù)據(jù)結(jié)構(gòu)。
測試和驗證
可能會有的同學(xué)會想體驗 Go1.23 的新特性,驗證這個 time.After 的修復(fù)是否有效。要特別注意下面這一點。
我們還是用前面提到的問題代碼來測試。但如果你直接在本地復(fù)用,可能不一定能生效,會看到還是有內(nèi)存泄露的情況。
主要是兩個原因,如下:
1、你要下載 Go 新版本并使用 Go1.23 運行:
// 安裝 go1.23rc2 的 go 新版本
$ go install golang.org/dl/go1.23rc2@latest
$ go1.23rc2 download
// 運行煎魚前面的代碼例子
$ go1.23rc2 run main.go
2、項目的 go.mod 文件注意 go 版本在 1.23,否則該新特性將由于兼容性保障無法生效:
圖片
運行一段時間后,之前的代碼中 Go1.23rc2 下內(nèi)存情況基本正常:
圖片
總結(jié)
今天給大家分享了一個花了將近 10 年,Go 才解決的計時器泄露問題。為此還是要給 rsc 點贊的,至少一直都有記著。就是這個解決速度比較慢,很多人在真實的 Go 工程中都已經(jīng)遇到過了。
另外從新版本開始,大家在舊項目體驗新特性是,要注意項目 go.mod 的 go 行版本或是 go toolchain 版本,避免由于版本過低而無法測試到真實的新特性效果。