Go 1.23 的 Timer 相关改动

photo by Cristina Gottardi on Unsplash

We should not judge people by their peak of excellence; but by the distance they have traveled from the point where they started.
— Henry Ward Beecher

Go 1.23 includes a new implementation of the channel-based timers created by time.NewTimertime.Aftertime.NewTicker, and time.Tick.

Timers 的关键改动

增强的垃圾回收

  1. 在以前的 Go 版本(1.23 之前)中,如果程序没有使用 Stop 方法显式停止,未停止的 time.Timers 和 time.Tickers 仍保留在内存中。这可能会导致资源泄漏,尤其是在长时间运行的程序中。
  2. Go 1.23 通过使未引用的 time.Timers 和 time.Tickers 有资格立即进行垃圾回收来解决这个问题。这确保了高效的内存管理,并防止不必要的资源使用。

通过 defer 等方法调用 Stop 显示释放资源显然是一个好习惯,但是在 1.23 之前,如果不小心忘了释放资源,或许会造成不可挽回的后果。

Go 对于这种粗心的行为做了进一步的防护,增强了代码的健壮性(robustness),显然是一个值得称赞的决定。如果你调用1.23中的 NewTicker 方法,将会看到其中的注释:

1
2
3
4
5
6
7
8
9
10
11
// Before Go 1.23, the garbage collector did not recover
// tickers that had not yet expired or been stopped, so code often
// immediately deferred t.Stop after calling NewTicker, to make
// the ticker recoverable when it was no longer needed.
// As of Go 1.23, the garbage collector can recover unreferenced
// tickers, even if they haven't been stopped.
// The Stop method is no longer necessary to help the garbage collector.
// (Code may of course still want to call Stop to stop the ticker for other reasons.)
func NewTicker(d Duration) *Ticker {
...
}

src/time/tick.go - go - Git at Google

channel 的同步性

  • 在 Go 1.23 之前,用于 time.Timer 和 time.Ticker 的计时器通道的缓冲区容量为 1。这可能会在使用 Reset 或 Stop 方法时带来挑战,因为调用后可能会传递过时的值。
  • Go 1.23 引入了同步定时器通道(无缓冲,容量为 0)。这有效地防止了对 Reset 或 Stop 的调用后在通道上进一步发送或接收任何的信号。这简化了有关计时器行为的推理,并消除了陈旧值的潜在问题。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"time"
)

func main() {
timer := time.NewTimer(2 * time.Second)
go func() {
select {
case <-timer.C:
fmt.Println("Timer expired")
case <-time.After(1 * time.Second):
fmt.Println("Timeout waiting for timer")
}
}()

time.Sleep(3 * time.Second) // Main goroutine sleeps for 3 seconds
if !timer.Stop() { // Check if timer was stopped
<-timer.C // Try to read from the timer's channel (goroutine 2)
fmt.Println("Timer was stopped but had to read expired value")
} else {
fmt.Println("Timer was stopped successfully")
}
}
Output:
// 1.22
Timeout waiting for timer
Timer was stopped but had to read expired value

// 1.23
Timeout waiting for timer
Timer was stopped successfully

Run Code in Go Dev

对现有代码的影响

兼容性

  • 仅当程序的 go.mod 文件指定 Go 1.23.0 或更高版本时,才会启用新的计时器行为。如果您使用 Go 1.23 构建较旧的代码,则以前的计时器行为仍然适用。
  • 要恢复到异步通道行为(即使使用 Go 1.23 或更高版本),您可以使用 GODEBUG 设置:GODEBUG=asynctimerchan=1。

通道长度

如果您的代码依赖于检查通道长度(使用 len)来确定接收操作是否成功,则需要将其修改为使用非阻塞接收。由于通道现在是无缓冲的,len 和 cap 将始终返回 0。

举个例子,在go 1.23 你需要将以下逻辑

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
timer := time.NewTimer(2 * time.Second)

time.Sleep(1 * time.Second)
if len(timer.C) > 0 {
fmt.Println("Timer channel has a value")
} else {
fmt.Println("Timer channel is empty")
}

timer.Stop()
}

改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
timer := time.NewTimer(2 * time.Second)

time.Sleep(1 * time.Second)
select {
case <-timer.C:
fmt.Println("Timer channel has a value")
default:
fmt.Println("Timer channel is empty")
}

timer.Stop()
}

调度时延

在 Go1.23 之前,一个 timer 创建时是有调度时延的,而最新的计时器实现不再受调度时延的影响,所以该更改对已有代码也可能有影响。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"time"
)

func main() {
for i := 1; i < 10; i++ {
c := make(chan bool)
close(c)

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

Output
// 1.22
done
done
done
done
done
done
done
done
done

// 1.23
done
timeout
done
done
done
timeout
timeout
timeout
timeout

Run Code in Go Dev

总结

  • 改进了未引用 the channel-based timers 的垃圾收集。
  • 异步定时器通道改为同步定时器通道。
  • 消除了创建 the channel-based timers 的调度时延

总之,这些更改会改进 Go 1.23 中的计时器行为,促进更好的资源管理以及更清晰的 Reset 和 Stop 语义。
但也会带来潜在隐患,如果您遇到旧代码的兼容性问题,请考虑使用 GODEBUG 设置或更新代码以适当处理无缓冲(阻塞)通道等场景。

参考

Go Wiki: Go 1.23 Timer Channel Changes - The Go Programming Language

更多该系列文章,参考medium链接:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

English post: https://programmerscareer.com/golang-timer-changes/
作者:Wesley Wei – Twitter Wesley Wei – Medium
注意:原文在 2024-07-06 23:39 时创作于 https://programmerscareer.com/golang-timer-changes/. 本文为作者原创,转载请注明出处。

Golang 中你应该知道的 WaitGroup 知识 golang中你应该知道的指针知识

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×