Go 1.23 对 time.Timer
和 time.Ticker
的实现进行了两项重大更改。
首先,如果程序中不再引用某个 Timer
或 Ticker
,则这些定时器或计时器会立即成为垃圾回收的候选对象,即使它们的 Stop
方法尚未被调用,也会被垃圾回收掉。在 Go 的早期版本中,未停止的 Timer
只有在触发后才会被回收,而未停止的 Ticker
则永远不会被回收。
其次,与 Timer
或 Ticker
关联的定时器通道现在变为无缓冲的,容量为 0。这一更改的主要效果是,Go 现在保证对于任何调用Reset
或 Stop
方法的操作,在该调用之前准备的任何过时值都不会在调用后被发送或接收。在 Go 的早期版本中,这些通道使用了一个元素的缓冲区,这使得正确使用 Reset
和 Stop
变得困难。这一更改的一个明显效果是,现在定时器通道的 len
和 cap
将返回 0
而不是 1
,这可能会影响那些通过轮询长度来决定定时器通道上的接收操作是否会成功的程序。此类代码应该改用非阻塞接收。
这些新行为仅在主 Go 程序位于使用 Go 1.23.0
或更高版本的 go.mod 文件的模块中时才会启用。当 Go 1.23
构建旧程序时,旧行为仍然有效。新的 GODEBUG 设置 asynctimerchan=1
可以在即使程序在其 go.mod 文件中指定了 Go 1.23.0
或更高版本时,也恢复到异步通道行为。
这是一个好的更改,解决了先前使用 Timer 或者 Ticker 的两个问题:一是不能及时垃圾回收,二是调用Reset
和Stop
会有潜在的问题。这个更改将会使得 Timer 和 Ticker 更加容易使用。
但是,如上面所说,Timer 和 Ticker 的通道真的是无缓冲的吗?其实 Russ Cox 在这里的实现是通过Hack
的方式实现的(runtime/chan.go):
func chanlen(c *hchan) int {
if c == nil {
return 0
}
async := debug.asynctimerchan.Load() != 0
if c.timer != nil && async {
c.timer.maybeRunChan()
}
if c.timer != nil && !async {
// timer channels have a buffered implementation
// but present to users as unbuffered, so that we can
// undo sends without users noticing.
return 0
}
return int(c.qcount)
}
看这段代码,你能嗅出什么味道吗?这也成了最近一些 Gopher 继批评iter
之后的第二次对 Russ Cox 的"攻击"。
首先,这段代码将 channle 的调用者(使用者)timer 关联到 channel 的实现,这是一个明显常识的设计,不仅仅这段代码,整个 channel 的实现中都弥漫着 Timer 的气息。
其次,这里对 timer 类型进行的一个虚假的处理,即使 timer 使用的 channel 是有缓冲的,这里也返回 0,欺骗调用者说它是无缓冲的,这是一个非常不好的设计,这样的设计会让调用者误解 channel 的实现,以及对后续的代码维护也不友好。
反正,twitter 又是一阵骂声。
另外,因为Timer
不被引用后,是不是下面的代码就不会出现 Go 1.23 之前的问题了呢(参考老貘的推文)?
func consumer(ctx context.Context, in <-chan token) {
const timeout = time.Hour
for {
select {
case <-ctx.Done():
return
case <-time.After(timeout):
log.Println("timeout")
case <-in:
// do something
}
}
}
依照 Go 1.23 的新特性,time.After
返回的Timer
不会被引用,那么这个Timer
会被垃圾回收掉,不像之前甚至会存活 1 个小时才被回收。
理论上是这样子的,但是这里还是有一个问题:time.After
返回的Timer
直到下一次垃圾回收才会被回收掉,这个时间是不确定的,所以这里还是有可能会出现问题,只是可能催货的时间变小了。
所以,安起见,这里还是要使用NewTimer
+ Reset
的方式来实现。
func consumer(ctx context.Context, in <-chan token) {
const timeout = time.Hour
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
log.Println("timeout")
case <-in:
// do something
if !timer.Stop() {
<-timer.C
}
timer.Reset(timeout)
}
}
}
推荐阅读:
想要了解Go更多内容,欢迎扫描下方👇关注公众号,扫描 [实战群]二维码 ,即可进群和我们交流~
- 扫码即可加入实战群 -
分享、在看与点赞Go