震惊! Go 1.23中Timer无buffer的实现方式竟是这样!

Go 1.23 对 time.Timertime.Ticker 的实现进行了两项重大更改。

首先,如果程序中不再引用某个 TimerTicker,则这些定时器或计时器会立即成为垃圾回收的候选对象,即使它们的 Stop 方法尚未被调用,也会被垃圾回收掉。在 Go 的早期版本中,未停止的 Timer 只有在触发后才会被回收,而未停止的 Ticker永远不会被回收

其次,与 TimerTicker 关联的定时器通道现在变为无缓冲的,容量为 0。这一更改的主要效果是,Go 现在保证对于任何调用ResetStop 方法的操作,在该调用之前准备的任何过时值都不会在调用后被发送或接收。在 Go 的早期版本中,这些通道使用了一个元素的缓冲区,这使得正确使用 ResetStop 变得困难。这一更改的一个明显效果是,现在定时器通道的 lencap 将返回 0 而不是 1,这可能会影响那些通过轮询长度来决定定时器通道上的接收操作是否会成功的程序。此类代码应该改用非阻塞接收。

这些新行为仅在主 Go 程序位于使用 Go 1.23.0 或更高版本的 go.mod 文件的模块中时才会启用。当 Go 1.23 构建旧程序时,旧行为仍然有效。新的 GODEBUG 设置 asynctimerchan=1 可以在即使程序在其 go.mod 文件中指定了 Go 1.23.0 或更高版本时,也恢复到异步通道行为。

这是一个好的更改,解决了先前使用 Timer 或者 Ticker 的两个问题:一是不能及时垃圾回收,二是调用ResetStop会有潜在的问题。这个更改将会使得 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)
        }
    }
}

- END -


推荐阅读:

6 个必须尝试的将代码转换为引人注目的图表的工具

Go 1.23新特性前瞻

Gopher的Rust第一课:第一个Rust程序

Go早期是如何在Google内部发展起来的

2024 Gopher Meetup 武汉站活动

go 中更加强大的 traces

「GoCN酷Go推荐」我用go写了魔兽世界登录器?

Go区不大,创造神话,科目三杀进来了

想要了解Go更多内容,欢迎扫描下方👇关注公众号,扫描 [实战群]二维码  ,即可进群和我们交流~


- 扫码即可加入实战群 -

cca091e0e94a6dd59d39095e33d308ea.jpeg

d22bf7bdf0b5d19b2f482d310d07650a.png

分享、在看与点赞Go fae6e5801bb7b54fa8a15b0d8c79923b.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值