Go Wiki: Go 1.23 定時器通道變更
Go 1.23 包含了一個由 time.NewTimer、time.After、time.NewTicker 和 time.Tick 建立的基於通道的定時器的全新實現。
新實現進行了兩項重要變更:
-
未停止且不再被引用的定時器和計時器可以被垃圾回收。在 Go 1.23 之前,未停止的定時器在觸發前無法被垃圾回收,而未停止的計時器則永遠無法被垃圾回收。Go 1.23 的實現避免了不使用
t.Stop的程式中的資源洩露。 -
定時器通道現在是同步的(無緩衝的),這使得
t.Reset和t.Stop方法具有更強的保證:在這些方法返回後,對定時器通道的任何後續接收都不會觀察到對應舊定時器配置的陳舊時間值。在 Go 1.23 之前,無法避免使用t.Reset時的陳舊值,並且避免使用t.Stop時的陳舊值需要謹慎使用t.Stop的返回值。Go 1.23 的實現完全消除了這一顧慮。
實現變更有兩個可觀察到的副作用,可能會影響生產行為或測試,詳見下文。
新實現僅用於 go.mod 中宣告 go 1.23 或更高版本的模組中的 main 包。其他程式將繼續使用舊的語義。GODEBUG 設定 asynctimerchan=1 會強制使用舊語義;反之,asynctimerchan=0 會強制使用新語義。
容量(Cap)和長度(Len)
在 Go 1.23 之前,定時器通道的 cap 是 1,而定時器通道的 len 表示是否有值在等待接收(有則為 1,無則為 0)。Go 1.23 的實現建立的定時器通道的 cap 和 len 始終為 0。
通常情況下,使用 len 來輪詢任何通道通常沒有太大幫助,因為另一個 goroutine 可能會同時從通道接收,從而隨時使 len 的結果失效。使用 len 輪詢定時器通道的程式碼應該改為使用非阻塞的 select。
也就是說,執行以下程式碼的程式
if len(t.C) == 1 {
<-t.C
more code
}
應該改為執行以下程式碼
select {
default:
case <-t.C:
more code
}
Select 競爭
在 Go 1.23 之前,使用非常短的間隔(如 0ns 或 1ns)建立的定時器,由於排程延遲,其通道準備好接收的時間會比該間隔長很多。這種延遲可以在以下程式碼中觀察到,該程式碼在 select 中選擇一個已準備好的通道和一個新建立的、具有非常短超時時間的定時器:
c := make(chan bool)
close(c)
select {
case <-c:
println("done")
case <-time.After(1*time.Nanosecond):
println("timeout")
}
當 select 引數被評估並且 select 檢視相關通道時,定時器應該已經過期,這意味著兩種情況都可以繼續執行。Select 透過隨機選擇一個來選擇多個就緒情況,所以這個程式應該大約一半時間選擇一種情況。
由於 Go 1.23 之前的定時器實現中的排程延遲,像這樣的程式會錯誤地 100% 執行“done”情況。
Go 1.23 的定時器實現不受相同的排程延遲影響,因此在 Go 1.23 中,該程式大約一半時間執行每種情況。
在 Google 程式碼庫中測試 Go 1.23 時,我們發現少數測試使用 select 將已準備好繼續執行的通道(通常是 context Done 通道)與具有非常低超時的定時器進行競爭。通常,生產程式碼會使用真實的超時時間,在這種情況下,競爭是無關緊要的,但為了測試,超時時間會被設定為一個非常小的值。然後,測試會堅持非超時情況執行,如果超時時間到了就會失敗。一個簡化的例子可能如下所示:
select {
case <-ctx.Done():
return nil
case <-time.After(timeout):
return errors.New("timeout")
}
然後測試會呼叫這段程式碼,並將 timeout 設定為 1ns,如果程式碼返回錯誤則會失敗。
要修復像這樣的測試,可以更改呼叫者使其能夠理解超時是可能的,或者像這樣更改程式碼以在超時情況下也優先選擇 done 通道:
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 的一部分。