当前位置:网站首页 > 更多 > 编程开发 > 正文

[编程技术] Golang time.After内存泄漏分析

作者:CC下载站 日期:2022-05-06 00:00:51 浏览:18 分类:编程开发

背景

我刚转做go语言开发开始写入职小程序的时候,写下了如下的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for {
    select {
        case conn := <- conns:
            ... //do someting
            return conn
        case <- time.After(c.Timeouot):
            ... //do something
            return xxx
    }
}

这只是一个简单的从连接池取连接的代码,如果连接池没有可用连接,就在超时后创建一个新的连接使用。

说实话,我觉得我的代码可优雅了,后来对go语言有了更深入的了解之后,发现我写的代码有着明显的内存泄漏的问题。

熟悉go语言的朋友可能已经看出来了,内存泄漏的代码就是time.After。 这是go语言经典的,或者说是对go语言机制不了解的初学者经常会踩的坑,稍不留神就会掉到坑里去。

今天我们就来分析一下这个time.After内存泄漏之谜。

我们来用最简单的例子模拟一下:

 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
package main

import (
	"fmt"
	"time"
)
//define a channel
var chs chan int

func Get() {
	for {
		select {
			case v := <- chs:
				fmt.Printf("print:%v\n", v)
			case <- time.After(3 * time.Minute):
				fmt.Printf("time.After:%v", time.Now().Unix())
		}
	}
}

func Put() {
	var i = 0
	for {
		i++
		chs <- i
	}
}

func main() {
	chs = make(chan int, 100)
	go Put()
	Get()
}

咱也别管代码优不优雅,就很简单的逻辑,先往channel里面存数据,然后不停的使用for select case语法从channel里面取数据。

我相信大家一眼就能看懂。就是这么简单的代码却会导致内存泄漏。

那么我们使用top指令看看: [编程技术] Golang time.After内存泄漏分析

发现我们的进程MEM这一列的值迅速增加,不到一分钟就占用了6G的内存,明显产生了内存泄漏。

原理分析

要了解为什么会产生内存泄漏,我们需要看看time package里面time.After函数的的定义。https://pkg.go.dev/time

func After(d Duration) <-chan Time

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.

该方法可以在一定时间(根据所传入的 Duration)后主动返回 time.Time 类型的 channel 消息。

注意: 描述里面写的很清楚:

  • 在计时器触发之前,垃圾收集器不会回收Timer
  • 如果考虑效率,需要使用NewTimer替代
1
2
3
4
5
6
7
8
for {
		select {
			case v := <- chs:
				fmt.Printf("print:%v\n", v)
			case <- time.After(3 * time.Minute):
				fmt.Printf("time.After:%v", time.Now().Unix())
		}
	}

回过头来看我们的代码,这里我们的定时时间设置的是3分钟, 在for循环内每次select的时候,都会实例化一个一个新的定时器。该定时器在3分钟后,才会被激活,但是激活后已经跟select无引用关系,被gc给清理掉。

重点是: 在select里面虽然我们没有执行到time.After,但是这个对象已经初始化了,依然在时间堆里面,定时任务未到期之前,是不会被gc清理的。

解决方案

根据time.After的解释,我们知道,for select case里面最好不要用time.After,go语言对这个方法的设计决定了它不能这么用,否则会有内存泄漏的风险。

至于解决方案,time package的文档也说明了,使用NewTimer来做定时器,不需要每次都创建定时器对象。

time.After虽然调用的是timer定时器,但是他没有使用time.Reset()方法再次激活定时器,所以每一次都是新创建的实例,才会造成的内存泄漏。

而我们使用NewTimer创建定时器,再加上time.Reset每次重新激活定时器,即可完美解决问题。

1
func NewTimer(d Duration) *Timer

NewTimer creates a new Timer that will send the current time on its channel after at least duration d.

Timer的指针里面封装了:

1
2
3
type Timer struct {
	C <-chan Time
}

The Timer type represents a single event. When the Timer expires, the current time will be sent on C, unless the Timer was created by AfterFunc. A Timer must be created with NewTimer or AfterFunc.

我们改造一下上面的Get函数,使用time.NewTimer来初始化Timer的指针,并用Reset来重置定时器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func Get() {
	delay := time.NewTimer(3 * time.Minute)

	defer delay.Stop()

	for {
		delay.Reset(3 * time.Minute)

		select {
			case v := <- chs:
				fmt.Printf("print:%v\n", v)
			case <- delay.C:
				fmt.Printf("time.After:%v", time.Now().Unix())
		}
	}
}

记得一定要使用Reset重置定时器,如果不重置,那么定时器还是从创建的时候开始计算时间流逝。使用了Reset之后,每次都从当前Reset的时间开始算。

这么改之后,再次测试代码,发现内存一直稳定。

总结

for select case里面使用定时器一定要小心,定时器只有等到计时器触发之后才会被垃圾回收器回收,而time.After是单次触发且无法Reset。

这种需要循环内触发定时器的用例,还是要使用time.NewTimer并手动Reset

<全文完>

您需要 登录账户 后才能发表评论

取消回复欢迎 发表评论:

关灯