Go Wiki:Go 1.23 計時器通道變更

Go 1.23 包含由 time.NewTimertime.Aftertime.NewTickertime.Tick 建立的基於通道的計時器的全新實作。

新的實作進行了兩項重要的變更

  1. 不再被參照的未停止計時器和提示器可以被垃圾回收。在 Go 1.23 以前,未停止計時器在計時器響起之前無法被垃圾回收,而未停止提示器永遠無法被垃圾回收。Go 1.23 實作可避免程式中沒有使用 t.Stop 造成的資源浪費。

  2. 計時器通道現在已同步(未緩衝),為 t.Resett.Stop 方法提供更強的保證:在這些方法之一傳回後,從計時器通道接收的任何後續資料都不會觀察到與舊計時器組態對應的舊時間值。在 Go 1.23 之前,無法避免使用 t.Reset 產生的舊值,而要避免使用 t.Stop 產生的舊值,則需要謹慎使用 t.Stop 傳回的值。Go 1.23 實作完全消除了這個問題。

實作變更具有兩個可觀察到的副作用,可能影響製作行為或測試,說明如下。

新的實作僅用於封裝主程式在宣告 go 1.23 或之後版本的 go.mod 的模組中。其他程式繼續使用舊語意。asynctimerchan=1 GODEBUG 設定 會強制執行舊語意;相反地,asynctimerchan=0 會強制執行新的語意。

Cap 和 Len

在 Go 1.23 之前,計時器通道的 cap 為 1,而計時器通道的 len 指出是否有值正在等待接收(如果有的話為 1,沒有的話為 0)。Go 1.23 實作會建立 caplen 始終為 0 的計時器通道。

一般來說,使用 len 輪詢任何通道通常沒有幫助,因為另一個執行緒可能會同時從該通道接收資料,隨時使 len 的結果失效。使用 len 輪詢計時器通道的程式碼應該使用非封鎖選取。

也就是說,執行下列動作的程式碼

if len(t.C) == 1 {
    <-t.C
    more code
}

應該改為執行下列動作

select {
default:
case <-t.C:
    more code
}

選取競爭

在 Go 1.23 之前,使用非常短的時間間隔建立的計時器(例如 0ns 或 1ns)會花費比該時間間隔明顯更長的時間,才能讓其通道準備好接收,這是因為排程延遲所致。可在選取已在選取之前準備好的通道和使用非常短暫時逾時的新建立計時器的程式碼中觀察到延遲

c := make(chan bool)
close(c)

select {
case <-c:
    println("done")
case <-time.After(1*time.Nanosecond):
    println("timeout")
}

當評估選取引數並在選取中查看相關通道時,計時器應該已逾時,這表示兩個案例都準備好進行。選取會從多個準備好的案例中進行選取,透過隨機選擇其中一個來進行,因此這個程式應該會在幾乎一半的時間中選取每個案例。

由於在 Go 1.23 之前的計時器實作中出現排程延遲,類似這樣的程式會一直錯誤執行「已完成」的案例。

Go 1.23 計時器實作不受相同的排程延遲影響,因此,在 Go 1.23 中,此程式會在幾乎一半的時間中執行每個案例。

在 Google 程式碼基礎架構中測試 Go 1.23 時,我們發現許多測試使用選取對準備進行中的通道(通常是內容 Done 通道)與逾時時間非常短的計時器進行競爭。一般而言,製作程式碼會使用實際逾時,這種情況下競爭並無意義,但在測試中,逾時會設定為非常小的值。然後,測試會堅持執行非逾時案例,如果達到逾時,則會失敗。簡化的範例可能如下所示

select {
case <-ctx.Done():
    return nil
case <-time.After(timeout):
    return errors.New("timeout")
}

然後測試會使用 `timeout` 設定為 1ns 來呼叫此程式碼,如果程式碼傳回錯誤,測試會失敗。

要修復此類測試,呼叫端可以變更為了解超時是可能的,或者程式碼可以變更为即便在超時的情況下也優先使用 `done` Channel,如下所示

select {
case <-ctx.Done():
    return nil
case <-time.After(timeout):
    // Double-check that Done is not ready,
    // in case of short timeout during test.
    select {
    default:
    case <-ctx.Done():
        return nil
    }
    return errors.New("timeout")
}

偵錯

如果程式或測試在使用 Go 1.23 時失敗但在使用 Go 1.22 時運作良好,可以利用 `asynctimerchan` GODEBUG 設定 來檢查新的計時器實現是否誘發失敗

GODEBUG=asynctimerchan=0 mytest  # force Go 1.23 timers
GODEBUG=asynctimerchan=1 mytest  # force Go 1.22 timers

如果程式或測試在使用 Go 1.22 時始終傳遞但在使用 Go 1.23 時始終失敗,這明確表示問題與計時器有關。

在我們觀察過的所有測試失敗中,問題都在測試本身,而非計時器實現,因此下一步是精確找出 `mytest` 中哪一個程式碼依賴舊實現。您能使用 `bisect` 工具 來執行此動作

go install golang.org/x/tools/cmd/bisect@latest
bisect -godebug asynctimerchan=1 mytest

執行此方式時,`bisect` 會重複執行 mytest,根據導致計時器呼叫的堆疊追蹤來開啟或關閉新的計時器實現。透過二元搜尋,它會將誘發的失敗縮小為在特定堆疊追蹤期間啟用新的計時器,並報告該追蹤。`bisect` 在執行時,會列印關於其嘗試的狀態訊息,主要是讓您在測試速度很慢時也能知道它仍在執行。

範例 `bisect` 執行類似如下所示

$ bisect -godebug asynctimerchan=1 ./view.test
bisect: checking target with all changes disabled
bisect: run: GODEBUG=asynctimerchan=1#n ./view.test... FAIL (7 matches)
bisect: run: GODEBUG=asynctimerchan=1#n ./view.test... FAIL (7 matches)
bisect: checking target with all changes enabled
bisect: run: GODEBUG=asynctimerchan=1#y ./view.test... ok (7 matches)
bisect: run: GODEBUG=asynctimerchan=1#y ./view.test... ok (7 matches)
bisect: target fails with no changes, succeeds with all changes
bisect: searching for minimal set of disabled changes causing failure
bisect: run: GODEBUG=asynctimerchan=1#!+0 ./view.test... FAIL (3 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+0 ./view.test... FAIL (3 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+00 ./view.test... ok (1 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+00 ./view.test... ok (1 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+10 ./view.test... FAIL (2 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+10 ./view.test... FAIL (2 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+0010 ./view.test... ok (1 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+0010 ./view.test... ok (1 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+1010 ./view.test... FAIL (1 matches)
bisect: run: GODEBUG=asynctimerchan=1#!+1010 ./view.test... FAIL (1 matches)
bisect: confirming failing change set
bisect: run: GODEBUG=asynctimerchan=1#v!+x65a ./view.test... FAIL (1 matches)
bisect: run: GODEBUG=asynctimerchan=1#v!+x65a ./view.test... FAIL (1 matches)
bisect: FOUND failing change set
--- change set #1 (disabling changes causes failure)
internal/godebug.(*Setting).Value()
    go/src/internal/godebug/godebug.go:165
time.syncTimer()
    go/src/time/sleep.go:25
time.NewTimer()
    go/src/time/sleep.go:144
time.After()
    go/src/time/sleep.go:202
region_dash/regionlist.(*Cache).Top()
    region_dash/regionlist/regionlist.go:89
region_dash/view.(*Page).ServeHTTP()
    region_dash/view/view.go:45
region_dash/view.TestServeHTTPStatus.(*Router).Handler.func2()
    httprouter/httprouter/params_go17.go:27
httprouter/httprouter.(*Router).ServeHTTP()
    httprouter/httprouter/router.go:339
region_dash/view.TestServeHTTPStatus.func1()
    region_dash/view/view.test.go:105
testing.tRunner()
    go/src/testing/testing.go:1689
runtime.goexit()
    go/src/runtime/asm_amd64.s:1695

---
bisect: checking for more failures
bisect: run: GODEBUG=asynctimerchan=1#!-x65a ./view.test... ok (6 matches)
bisect: run: GODEBUG=asynctimerchan=1#!-x65a ./view.test... ok (6 matches)
bisect: target succeeds with all remaining changes disabled

在此案例中,堆疊追蹤明確指出在使用新的計時器時,哪個呼叫 `time.After` 會導致失敗。


此內容是 Go Wiki 的一部分。