好多年前,我写过 timer.After 的使用和坑。Go 这么多年以来这块一直有内存泄露。有的同学或多或少都有遇到过。最近 Go1.23 即将正式发布,Go 核心团队负责人 rsc 自述花了将近 10 年的努力,终于把这个问题修复了。值得我们关注!
timer.After 是什么
这是之前编写的部分,我测试验证了下。在 Go1.22 依然有效,仍然是有问题的。因此没有做什么修改。主要是给大家做知识温习回顾的作用。今天是男主角是 Go 标准库 time 所提供的 After 方法。函数签名如下:
func After(d Duration) <-chan Time
该方法可以在一定时间(根据所传入的 Duration)后主动返回 time.Time 类型的 channel 消息。在常见的场景下,我们会基于此方法做一些计时器相关的功能开发,例子如下:
func main() {
ch := make(chan string)
go func() {
time.Sleep(time.Second * 3)
ch <- "脑子进煎鱼了"
}()
select {
case _ = <-ch:
case <-time.After(time.Second * 1):
fmt.Println("堆代码出去了,超时了!!!")
}
}
在运行 1 秒钟后,输出结果:堆代码出去了,超时了!!!
上述程序在在运行 1 秒钟后将触发 time.After 方法的定时消息返回,输出了超时的结果。
有什么问题和坑
从例子来看似乎非常正常,也没什么 “坑” 的样子。莫非是虚晃一枪?我们再看一个不像是有问题例子,这在 Go 工程中经常能看见,只是大家都没怎么关注。
代码如下:
func main() {
ch := make(chan int, 10)
go func() {
in := 1
for {
in++
ch <- in
}
}()
for {
select {
case _ = <-ch:
// 堆代码 duidaima.com
continue
case <-time.After(3 * time.Minute):
fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())
}
}
}
在上述代码中,我们构造了一个 for+select+channel 的一个经典的处理模式。同时在 select+case 中调用了 time.After 方法做超时控制,避免在 channel 等待时阻塞过久,引发其他问题。看上去都没什么问题,但是细心一看。在运行了一段时间后,我的笔记本电脑已经温热了许多。
粗暴的利用 top 命令一看:
例子中 Go 工程的内存占用竟然已经达到了 30+GB 之高,并且还在持续增长。在再等待了一段时间后(所设置的超时时间到达),Go 工程的内存占用也没有要恢复合理的数值。这非常可怕。这明显就是存在内存泄露的问题。
问题原因
这个内存泄露的问题,无容置疑是 Go 官方认可的 BUG。快速的用一句话来讲,核心原因在于:for select 已结束,无法被 GC,时间堆内的被触发的计时器还在。
Go1.23 timer.After 不泄露了!
在现在 2024 年,经过将近十年的努力,Go 核心团队负责人 rsc 终于解决了这个问题!!!
自 Go1.23 版本起,会对用于计时器的通道(或者可能是用于通道的计时器)进行特殊处理,以便当没有通道操作待处理时,计时器将不会存放在计时器堆中。这意味着当一旦不再引用通道和计时器,就可以对其进行 GC,不必等待计时器到期或明确停止计时器。
注:这里的计时器是指 time.After、time.NewTimer 和 time.NewTicker 使用的数据结构。
测试和验证
可能会有的同学会想体验 Go1.23 的新特性,验证这个 time.After 的修复是否有效。要特别注意下面这一点。我们还是用前面提到的问题代码来测试。但如果你直接在本地复用,可能不一定能生效,会看到还是有内存泄露的情况。
主要是两个原因,如下:
1、你要下载 Go 新版本并使用 Go1.23 运行:
// 安装 go1.23rc2 的 go 新版本
$ go install golang.org/dl/go1.23rc2@latest
$ go1.23rc2 download
// 运行煎鱼前面的代码例子
$ go1.23rc2 run main.go
2、项目的 go.mod 文件注意 go 版本在 1.23,否则该新特性将由于兼容性保障无法生效:
运行一段时间后,之前的代码中 Go1.23rc2 下内存情况基本正常:
总结
今天给大家分享了一个花了将近 10 年,Go 才解决的计时器泄露问题。为此还是要给 rsc 点赞的,至少一直都有记着。就是这个解决速度比较慢,很多人在真实的 Go 工程中都已经遇到过了。另外从新版本开始,大家在旧项目体验新特性是,要注意项目 go.mod 的 go 行版本或是 go toolchain 版本,避免由于版本过低而无法测试到真实的新特性效果。