Go1.23 新特性:花了近 10 年,time.After 終于不泄漏了!
大家好,我是煎魚。
好多年前,我寫過 timer.After 的使用和坑。Go 這么多年以來這塊一直有內存泄露。有的同學或多或少都有遇到過。
最近 Go1.23 即將正式發布,Go 核心團隊負責人 rsc 自述花了將近 10 年的努力,終于把這個問題修復了。值得我們關注!
timer.After 是什么
這是之前編寫的部分,我測試驗證了下。在 Go1.22 依然有效,仍然是有問題的。因此沒有做什么修改。主要是給大家做知識溫習回顧的作用。
今天是男主角是 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 方法的定時消息返回,輸出了超時的結果。
有什么問題和坑
從例子來看似乎非常正常,也沒什么 “坑” 的樣子。莫非是虛晃一槍?
我們再看一個不像是有問題例子,這在 Go 工程中經常能看見,只是大家都沒怎么關注。
代碼如下:
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("現在是:%d,我腦子進煎魚了!", time.Now().Unix())
}
}
}
在上述代碼中,我們構造了一個 for+select+channel 的一個經典的處理模式。
同時在 select+case 中調用了 time.After 方法做超時控制,避免在 channel 等待時阻塞過久,引發其他問題。
看上去都沒什么問題,但是細心一看。在運行了一段時間后,我的筆記本電腦已經溫熱了許多。
粗暴的利用 top 命令一看:
圖片
例子中 Go 工程的內存占用竟然已經達到了 30+GB 之高,并且還在持續增長。在再等待了一段時間后(所設置的超時時間到達),Go 工程的內存占用也沒有要恢復合理的數值。這非常可怕。
這明顯就是存在內存泄露的問題。
問題原因
這個內存泄露的問題,無容置疑是 Go 官方認可的 BUG。
快速的用一句話來講,核心原因在于:for select 已結束,無法被 GC,時間堆內的被觸發的計時器還在。
Go 官方文檔說明
如果是想深入看原因可以查看以前我寫的《Go 內存泄露之痛,這篇把 Go timer.After 問題根因講透了!》
Go1.23 timer.After 不泄露了!
在現在 2024 年,經過將近十年的努力,Go 核心團隊負責人 rsc 終于解決了這個問題!!!
圖片
自 Go1.23 版本起,會對用于計時器的通道(或者可能是用于通道的計時器)進行特殊處理,以便當沒有通道操作待處理時,計時器將不會存放在計時器堆中。
這意味著當一旦不再引用通道和計時器,就可以對其進行 GC,不必等待計時器到期或明確停止計時器。
注:這里的計時器是指 time.After、time.NewTimer 和 time.NewTicker 使用的數據結構。
測試和驗證
可能會有的同學會想體驗 Go1.23 的新特性,驗證這個 time.After 的修復是否有效。要特別注意下面這一點。
我們還是用前面提到的問題代碼來測試。但如果你直接在本地復用,可能不一定能生效,會看到還是有內存泄露的情況。
主要是兩個原因,如下:
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 下內存情況基本正常:
圖片
總結
今天給大家分享了一個花了將近 10 年,Go 才解決的計時器泄露問題。為此還是要給 rsc 點贊的,至少一直都有記著。就是這個解決速度比較慢,很多人在真實的 Go 工程中都已經遇到過了。
另外從新版本開始,大家在舊項目體驗新特性是,要注意項目 go.mod 的 go 行版本或是 go toolchain 版本,避免由于版本過低而無法測試到真實的新特性效果。