前言
用golang已经有一段时间了,中间用到了定时器,也在实践了不少go经典的for+select模型,一直不太明白,正好最近有一个自己挖的坑——服务cpu占用率极高。初步定位了一下应该是由于定时器的使用不当导致的cpu占用率居高不下的情况。这边简单记录一下golang定时器的一些使用陷阱和正确的姿势,以需求的方式描述并分析记录。
需求
需求一:实现定时打印一句话
需求二:定时打印一句话,并同时累计一个数值
一、需求一
golang中提供了两种“定时器”,ticker
和timer
,两种虽然都能实现定时的功能,但是详细上是有区别的,简单说:
timer
是一个一次性的闹钟,时间到了执行完操作就不再执行(手动重置可以继续执行)
ticker
是一个定时闹钟,每到设定的时间就执行,一直会执行下去
既然这样的话我们分别使用两种定时器来实现这个功能,详细的代码
1 timer实现
1.1 错误的实现方式
先说结论,本代码实现中,当间隔的时间比较小时,比如5ms间隔,会发生内存泄露的问题,demo的内存一直持续增高。
func timer1(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-time.After(time.Millisecond * 5):
fmt.Printf("%v 是时候了\n", time.Now().Format(time.ANSIC))
}
}
}
1.2 正确的实现方式
func timer2(ctx context.Context) {
fmt.Printf("%v 现在还不是时候\n", time.Now().Format(time.ANSIC))
t := time.NewTimer(time.Second * 5)
defer t.Stop()
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-t.C:
//fmt.Printf("%v 是时候了\n", time.Now().Format(time.ANSIC))
t.Reset(time.Millisecond * 5)
}
}
}
1.3 错误分析
1.1中代码case <-time.After(time.Millisecond * 5):
部分是存在内存泄露的关键,我们查看一下源码:
每个After函数都会New一个新的对象,时间间隔太小时,新创建的对象未被释放掉,导致内存泄露。
2 ticker实现
2.1 错误的实现方式
先说结论,本代码实现中,当间隔的时间比较小时,比如5ms间隔,会发生内存泄露的问题,demo的内存一直持续增高。
func timer3(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-time.Tick(time.Millisecond * 5):
fmt.Printf("%v 是时候了\n", time.Now().Format(time.ANSIC))
}
}
}
2.2 正确的实现方式
func timer4(ctx context.Context) {
fmt.Printf("%v 现在还不是时候\n", time.Now().Format(time.ANSIC))
t := time.NewTicker(time.Millisecond * 5)
defer t.Stop()
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-t.C:
//fmt.Printf("%v 是时候了\n", time.Now().Format(time.ANSIC))
}
}
}
2.3 错误分析
2.1中代码case <-time.Tick(time.Millisecond * 5):
同样是导致内存泄露的主要原因,源码:
二、需求二
任务分解之后是两个功能,定时打印,累计数据。定时打印经过上面我们知道了怎么去正确实现,剩下就是累计的功能了。
1 错误实现
1.1 实现1
先说结论:能累计,不会定时执行,但CPU占用率高
func work1(ctx context.Context) {
c := 0
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-time.After(time.Second * 5):
fmt.Printf("%v 差不多行了\n", time.Now().Format(time.ANSIC))
default:
c++
}
}
}
1.2 实现2
先说结论:能累计,会定时执行,但CPU占用率高
func work2(ctx context.Context) {
c := 0
tick := time.NewTicker(time.Second * 5)
defer tick.Stop()
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-tick.C:
fmt.Printf("%v 差不多行了\n", time.Now().Format(time.ANSIC))
default:
c++
}
}
}
2 正确实现
2.1 实现1
func work3(ctx context.Context) {
c := 0
tick := time.NewTicker(time.Second * 5)
defer tick.Stop()
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-tick.C:
fmt.Printf("%v 差不多行了\n", time.Now().Format(time.ANSIC))
default:
c++
time.Sleep(time.Nanosecond * 1)
}
}
}
2.2 实现2
func work4(ctx context.Context) {
c := 0
timer := time.NewTimer(time.Second * 5)
defer timer.Stop()
for {
select {
case <-ctx.Done():
fmt.Printf("work 退出")
return
case <-timer.C:
fmt.Printf("%v 差不多行了\n", time.Now().Format(time.ANSIC))
timer.Reset(time.Second * 5)
default:
c++
time.Sleep(time.Nanosecond * 1)
}
}
}
3 结果分析
对比上述1和2的实现,cpu高是因为在default中没有sleep,每到default会占用cpu不会释放,导致CPU很高。
三、拓展
在2.1小结中定义器的退出是通过判断传入的context参数来退出for循环。有些情况下不一定能够将服务退出的信号传递到比较深的业务定时器中,查看定时器的源码发现当到达定时器的时刻时,会在ticker的chan——tick.C中写入一个值。利用chan的特性我们可以修改一下代码的逻辑,当定时器stop时会自动退出for循环。
func main() {
var (
_count int32 = 0
)
logger := log.New(os.Stdout, "", log.Lshortfile|log.Ldate|log.Ltime)
tick := time.NewTicker(interval)
go func() {
for range tick.C {
logger.Printf("this is tick time:[%v]", atomic.LoadInt32(&_count))
atomic.AddInt32(&_count, 1)
}
}()
utils.HandlerExit(func(s os.Signal) int {
logger.Printf("receive get a signal %s", s.String())
tick.Stop()
logger.Printf(" service exit")
return 0
})
}