Go坑:time.After可能导致的内存泄露问题分析
Go 中 time.After 可能导致的内存泄露
一、Time 包中定时器函数
go v1.20.4
定时函数:NewTicker,NewTimer 和 time.After 介绍
time 包中有 3 个比较常用的定时函数:NewTicker,NewTimer 和 time.After:
- NewTimer: 表示在一段时间后才执行,默认情况下执行一次。如果想再次执行,需要调用 time.Reset() 方法,这时类似于 NewTicker 定时器了。可以调用 stop 方法停止执行。
func NewTimer(d Duration) *Timer
// NewTimer 创建一个新的 Timer,它将至少持续时间 d 之后,在向通道中发送当前时间
// d 表示间隔时间
type Timer struct {
C <-chan Time
r runtimeTimer
}
重置 NewTimer 定时器的 Reset()
方法,它是定时器在持续时间 d 到期后,用这个方法重置定时器让它再一次运行,如果定时器被激活返回 true,如果定时器已过期或停止,在返回 false。
func (t *Timer) Reset(d Duration) bool
- 用 Reset 方法需要注意的地方:
如果程序已经从 t.C 接收到了一个值,则已知定时器已过期且通道值已取空,可以直接调用 time.Reset 方法;
如果程序尚未从 t.C 接收到值,则要先停止定时器 t.Stop(),再从 t.C 中取出值,最后调用 time.Reset 方法。
综合上面 2 种情况,正确使用 time.Reset 方法就是:
if !t.Stop() {
<-t.C
}
t.Reset(d)
- Stop 方法
func (t *Timer) Stop() bool
// 如果定时器已经过期或停止,返回 false,否则返回 true
Stop 方法能够阻止定时器触发,但是它不会关闭通道,这是为了防止从通道中错误的读取值。
为了确保调用 Stop 方法后通道为空,需要检查 Stop 方法的返回值并把通道中的值清空,如下:
if !t.Stop() {
<-t.C
}
NewTicker: 表示每隔一段时间运行一次,可以执行多次。可以调用 stop 方法停止执行。
func NewTicker(d Duration) *Ticker
NewTicker 返回一个 Ticker,这个 Ticker 包含一个时间的通道,每次重置后会发送一个当前时间到这个通道上。
d
表示每一次运行间隔的时间。time.After: 表示在一段时间后执行。其实它内部调用的就是 time.Timer 。
func After(d Duration) <-chan Time
跟它还有一个相似的函数 time.AfterFunc
,后面运行的是一个函数。
NewTicker 代码例子:
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
done := make(chan bool)
go func() {
time.Sleep(10 * time.Second)
done <- true
}()
for {
select {
case <-done:
fmt.Println("Done!")
return
case t := <-ticker.C:
fmt.Println("Current time: ", t)
}
}
}
二、time.After 导致的内存泄露
基本用法
time.After 方法是在一段时间后返回 time.Time 类型的 channel 消息,看下面源码就清楚返回值类型:
// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL156C1-L158C2
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL50C1-L53C2
type Timer struct {
C <-chan Time
r runtimeTimer
}
从代码可以看出它底层就是 NewTimer
实现。
一般可以用来实现超时检测:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
ch1 <- "hello"
}()
select {
case res := <-ch1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout")
}
}
有问题代码
上面的代码运行是没有什么问题的,不会导致内存泄露。
那问题会出在什么地方?
在有些情况下,select 需要配合 for 不断检测通道情况,问题就有可能出在 for 循环这里。
修改上面的代码,加上 for + select,为了能显示的看出问题,加上 pprof + http 代码,
timeafter.go:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
fmt.Println("start...")
ch1 := make(chan string, 120)
go func() {
// time.Sleep(time.Second * 1)
i := 0
for {
i++
ch1 <- fmt.Sprintf("%s %d", "hello", i)
}
}()
go func() {
// http 监听8080, 开启 pprof
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("listen failed")
}
}()
for {
select {
case _ = <-ch1:
// fmt.Println(res)
case <-time.After(time.Minute * 3):
fmt.Println("timeout")
}
}
}
在终端上运行代码:go run timeafter.go
,
然后在开启另一个终端运行:go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap
,
运行之后它会自动在浏览器上弹出 pprof 的浏览界面,http://localhost:8081/ui/。
本机运行一段时间后比较卡,也说明程序有问题。可以在运行一段时间后关掉运行的 Go 程序,避免电脑卡死。
用pprof分析问题代码
在浏览器上查看 pprof 图,http://localhost:8081/ui/:
从上图可以看出,内存使用暴涨(不关掉程序还会继续涨)。而且暴涨的内存集中在 time.After 上,上面分析了 time.After 实质调用的就是 time.NewTimer,从图中也可以看出。它调用 time.NewTimer 不断创建和申请内存,何以看出这个?继续看下面分析,
再来看看哪段代码内存使用最高,还是用 pprof 来查看,浏览 http://localhost:8081/ui/source,
timeafter.go
上面调用的 Go 源码 NewTimer,
从上图数据分析可以看出最占用内存的那部分代码,src/time/sleep.go/NewTimer 里的 c 和 t 分配和申请内存,最占用内存。
如果不强行关闭运行程序,这里内存还会往上涨。
为什么会出现内存一直涨呢?
在程序中加了 for 循环,for 循环都会不断调用 select,而每次调用 select,都会重新初始化一个新的定时器 Timer(调用time.After,一直调用它就会一直申请和创建内存),这个新的定时器会增加到时间堆中等待触发,而定时器启动前,垃圾回收器不会回收 Timer(Go源码注释中有解释),也就是说 time.After 创建的内存资源需要等到定时器执行完后才被 GC 回收,一直增加内存 GC 却不回收,内存肯定会一直涨。
当然,内存一直涨最重要原因的还是 for 循环里一直在申请和创建内存,其它是次要 。
// https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go#LL150C1-L158C2
// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
// 在经过 d 时段后,会发送值到通道上,并返回通道。
// 底层就是 NewTimer(d).C。
// 定时器Timer启动前不会被垃圾回收器回收,定时器执行后才会被回收。
// 如果担心效率问题,可以使用 NewTimer 代替,如果不需要定时器可以调用 Timer.Stop 停止定时器。
在上面的程序中,time.After(time.Minute * 3) 设置了 3 分钟,也就是说 3 分钟后才会执行定时器任务。而这期间会不断被 for 循环调用 time.After,导致它不断创建和申请内存,内存就会一直往上涨。
那怎么解决循环调用的问题?解决了,就可能解决内存一直往上涨的问题。
解决问题
既然是 for 循环一直调用 time.After 导致内存暴涨问题,那不循环调用 time.After 行不行?
修改后的代码如下:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
fmt.Println("start...")
ch1 := make(chan string, 120)
go func() {
// time.Sleep(time.Second * 1)
i := 0
for {
i++
ch1 <- fmt.Sprintf("%s %d", "hello", i)
}
}()
go func() {
// http 监听8080, 开启 pprof
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("listen failed")
}
}()
// time.After 放到 for 外面
timeout := time.After(time.Minute * 3)
for {
select {
case _ = <-ch1:
// fmt.Println(res)
case <-timeout:
fmt.Println("timeout")
return
}
}
}
在终端上运行代码,go run timeafter1.go
,
等待半分钟左右,在另外一个终端上运行 go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap
,
自动在浏览器上弹出界面 http://localhost:8081/ui/ ,我这里测试,界面没有任何数据显示,说明修改后的程序运行良好。
在 Go 的源码中 After 函数注释说了为了更有效率,可以使用 NewTimer ,那我们使用这个函数来改造上面的代码,
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
fmt.Println("start...")
ch1 := make(chan string, 120)
go func() {
// time.Sleep(time.Second * 1)
i := 0
for {
i++
ch1 <- fmt.Sprintf("%s %d", "hello", i)
}
}()
go func() {
// http 监听8080, 开启 pprof
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("listen failed")
}
}()
duration := time.Minute * 2
timer := time.NewTimer(duration)
defer timer.Stop()
for {
timer.Reset(duration) // 这里加上 Reset()
select {
case _ = <-ch1:
// fmt.Println(res)
case <-timer.C:
fmt.Println("timeout")
return
}
}
}
在上面的实现中,也把 NewTimer 放在循环外面,并且每次循环中都调用了 Reset
方法重置定时时间。
测试,运行 go run timeafter1.go
,然后多次运行 go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap
,查看 pprof,我这里测试每次数据都是空白,说明程序正常运行。
三、网上一些错误分析
for循环每次select的时候,都会实例化一个一个新的定时器。该定时器在多少分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。换句话说,被遗弃的time.After定时任务还是在时间堆里面,定时任务未到期之前,是不会被gc清理的
上面这种分析说明,最主要的还是没有说清楚内存暴涨的真正内因。如果用 pprof 的 source 分析查看,就一目了然,那就是 NewTimer 里的 2 个变量创建和申请内存导致的。
四、参考
- https://pkg.go.dev/time#pkg-overview
- https://github.com/golang/go/blob/go1.20.4/src/time/sleep.go
- https://www.cnblogs.com/jiujuan/p/14588185.html pprof 基本使用
- 《100 Go Mistakes and How to Avoid Them》 作者:Teiva Harsanyi
Go坑:time.After可能导致的内存泄露问题分析的更多相关文章
- dotnet 6 在 Win7 系统证书链错误导致 HttpWebRequest 内存泄露
本文记录我将应用迁移到 dotnet 6 之后,在 Win7 系统上,因为使用 HttpWebRequest 访问一个本地服务,此本地服务开启 https 且证书链在此 Win7 系统上错误,导致应用 ...
- 五、jdk工具之jmap(java memory map)、 mat之四--结合mat对内存泄露的分析、jhat之二--结合jmap生成的dump结果在浏览器上展示
目录 一.jdk工具之jps(JVM Process Status Tools)命令使用 二.jdk命令之javah命令(C Header and Stub File Generator) 三.jdk ...
- (转)专项:Android 内存泄露实践分析
今天看到一篇关于Android 内存泄露实践分析的文章,感觉不错,讲的还算详细,mark到这里. 原文发表于:Testerhome: 作者:ycwdaaaa ; 原文链接:https://teste ...
- logging 模块误用导致的内存泄露
首先介绍下怎么发现的吧, 线上的项目日志是通过 logging 模块打到 syslog 里, 跑了一段时间后发现 syslog 的 UDP 连接超过了 8W, 没错是 8 W. 主要是 logging ...
- 深度:ARC会导致的内存泄露
iOS提供了ARC功能,很大程度上简化了内存管理的代码. 但使用ARC并不代表了不会发生内存泄露,使用不当照样会发生内存泄露. 下面列举两种内存泄露的情况. 1,循环参照 A有个属性参照B,B有个属性 ...
- C# 定时器导致的内存泄露问题
C# 中有三种定时器,System.Windows.Forms 中的定时器和 System.Timers.Timer 的工作方式是完全一样的,所以,这里我们仅讨论 System.Timers.Time ...
- 可能会导致.NET内存泄露的8种行为
原文连接:https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/作者 Michael Shpilt.授权翻译,转载请保 ...
- JavaScript之详述闭包导致的内存泄露
一.内存泄露 1. 定义:一块被分配的内存既不能使用,也不能回收.从而影响性能,甚至导致程序崩溃. 2. 起因:JavaScript的垃圾自动回收机制会按一定的策略找出那些不再继续使用的变量,释放其占 ...
- Java 程序的内存泄露问题分析
什么是内存泄露? 广义的Memory Leak:应用占用了内存,但是不再使用(包括不能使用)该部分内存 狭义的Memory Leak:应用分配了内存,但是不能再获取该部分内存的引用(对于Java,也不 ...
- Android中使用Handler造成内存泄露的分析和解决
什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...
随机推荐
- 基于5G/4G智能网关的大货车安全监测方案
大货车是我们身边最常见的货运车辆,从各种原材料到货物成品,都需要大大小小的货车承担过程中的运输工作.而由于货车通常载重多.体积大.行车盲区多,因此也产生较多的交通安全风险. 针对大货车的交通安全保障, ...
- 拖拽改变div宽、高(转)
$(function () { //绑定需要拖拽改变大小的元素对象 bindResize(document.getElementById('test')); }); function bindResi ...
- SXSSFWorkbook 表格内换行
起因 导出的excel需要在表格内换行,但搜索到的方法都实现不了我的需求,经同事搜查得知,这是POI的一个bug,已经在17年八月后被解决. 生成方式 pom依赖 <dependency> ...
- SQL字符匹配
一般形式 列名 [not] like 'str' 匹配串可以是以下四种通配符: 单下划线 _:匹配任意一个字符: %:匹配0个或多个字符: [ ]:匹配[ ]中的任意一个字符(若要比较的字符是连续的, ...
- 使用Wireshark查看HTTPS中TLS握手过程
通过使用Wireshark抓包分析TLS握手的过程,可以更容易理解和验证TLS协议,本文将先介绍Wireshark解密HTTPS流量的方法,然后分别验证TLS握手过程和TLS会话恢复的过程. 一.使用 ...
- 对利用jsp模板编写登录、注册界面的方法言
使用模板的相关操作步骤详解 1.可以在相关的网站上面找相关的css或者js文件,下载到一个特定的文件夹里面,以备使用 2.然后,将存有相关代码的文件夹直接复制粘贴到web文件下,就会直接保存,可以根据 ...
- web初始:html记忆
12.13html框架 <! DOCTYPE html> <html lang="zh-CN"> <head> <meta charset ...
- J - Straight Master Gym - 101775J 差分
题意:纸牌顺子:连续的3张或连续的4张或连续的5张为顺子.手中的牌共有n个数字,每个数字是a[i]个,能不能把手中所有的牌都是属于顺子. 1 ≤ T ≤ 100. 1 ≤ N ≤ 2 × 105. 0 ...
- P7213 [JOISC2020] 最古の遺跡 3 乱写
不想写题解了,把写在草稿纸上的东西整理了一下 感谢 crashed 大佬的题解与对本人问题的回答,没有他我就不会搞懂这道神仙计数题.
- crontab使用说明【一文搞懂Linux定时任务Crontab】
1.简介 cron是一个在后台运行调度的守护进程,而crontab是一个设置cron的工具.cron调度的是/etc/crontab文件. 2.centos安装crontab yum install ...