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.Aftertime.NewTimertime.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 版本,避免由於版本過低而無法測試到真實的新特性效果。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Qcpj7TqMeOwCs--59kD3Kw