golang 定时任务方面time.Sleep和time.Tick的优劣对比分析


Posted in Golang onMay 05, 2021

golang 写循环执行的定时任务,常见的有以下三种实现方式

1、time.Sleep方法:

for {
   time.Sleep(time.Second)
   fmt.Println("我在定时执行任务")
}

2、time.Tick函数:

t1:=time.Tick(3*time.Second)
for {
   select {
   case <-t1:
      fmt.Println("t1定时器")
   }
}

3、其中Tick定时任务

也可以先使用time.Ticker函数获取Ticker结构体,然后进行阻塞监听信息,这种方式可以手动选择停止定时任务,在停止任务时,减少对内存的浪费。

t:=time.NewTicker(time.Second)
for {
   select {
   case <-t.C:
      fmt.Println("t1定时器")
      t.Stop()
   }
}

其中第二种和第三种可以归为同一类

这三种定时器的实现原理

一般来说,你在使用执行定时任务的时候,一般旁人会劝你不要使用time.Sleep完成定时任务,但是为什么不能使用Sleep函数完成定时任务呢,它和Tick函数比,有什么劣势呢?这就需要我们去探讨阅读一下源码,分析一下它们之间的优劣性。

首先,我们研究一下Tick函数,func Tick(d Duration) <-chan Time

调用Tick函数会返回一个时间类型的channel,如果对channel稍微有些了解的话,我们首先会想到,既然是返回一个channel,在调用Tick方法的过程中,必然创建了goroutine,该Goroutine负责发送数据,唤醒被阻塞的定时任务。我在阅读源码之后,确实发现函数中go出去了一个协程,处理定时任务。

按照当前的理解,使用一个tick,需要go出去一个协程,效率和对内存空间的占用肯定不能比sleep函数强。我们需要继续阅读源码才拿获取到真理。

简单的调用过程我就不陈述了,我在这介绍一下核心结构体和方法(删除了部分判断代码,解释我写在表格中):

func (tb *timersBucket) addtimerLocked(t *timer) {
   t.i = len(tb.t)  //计算timersBucket中,当前定时任务的长度
   tb.t = append(tb.t, t)// 将当前定时任务加入timersBucket
   siftupTimer(tb.t, t.i)  //维护一个timer结构体的最小堆(四叉树),排序关键字为执行时间,即该定时任务下一次执行的时间
   if !tb.created {
      tb.created = true
      go timerproc(tb)// 如果还没有创建过管理定时任务的协程,则创建一个,执行通知管理timer的协程,最核心代码
   }
}

timersBucket,顾名思义,时间任务桶,是外界不可见的全局变量。每当有新的timer定时器任务时,会将timer加入到timersBucket中的timer切片。timerBucket结构体如下:

type timersBucket struct {
   lock         mutex //添加新定时任务时需要加锁(冲突点在于维护堆)
   t            []*timer //timer切片,构造方式为四叉树最小堆
}

func timerproc(tb *timersBucket) 详细介绍

可以称之为定时任务处理器,所有的定时任务都会加入timersBucket,然后在该函数中等待被处理。

等待被处理的timer,根据when字段(任务执行的时间,int类型,纳秒级别)构成一个最小堆,每次处理完成堆顶的某个timer时,会给它的when字段加上定时任务循环间隔时间(即Tick(d Duration) 中的d参数),然后重新维护堆,保证when最小的timer在堆顶。当堆中没有可以处理的timer(有timer,但是还不到执行时间),需要计算当前时间和堆顶中timer的任务执行时间差值delta,定时任务处理器沉睡delta段时间,等待被调度器唤醒。

核心代码如下(注释写在每行代码的后面,删除一些判断代码以及不利于阅读的非核心代码):

func timerproc(tb *timersBucket) {
   for {
      lock(&tb.lock) //加锁
      now := nanotime()  //当前时间的纳秒值
      delta := int64(-1)  //最近要执行的timer和当前时间的差值
      for {
         if len(tb.t) == 0 {
            delta = -1
            break
         }//当前无可执行timer,直接跳出该循环
         t := tb.t[0]
         delta = t.when - now //取when组小的的timer,计算于当前时间的差值
         if delta > 0 {
            break
         }// delta大于0,说明还未到发送channel时间,需要跳出循环去睡眠delta时间
         if t.period > 0 {
            // leave in heap but adjust next time to fire
            t.when += t.period * (1 + -delta/t.period)// 计算该timer下次执行任务的时间
            siftdownTimer(tb.t, 0) //调整堆
         } else {
            // remove from heap,如果没有设定下次执行时间,则将该timer从堆中移除(time.after和time.sleep函数即是只执行一次定时任务)
            last := len(tb.t) - 1
            if last > 0 {
               tb.t[0] = tb.t[last]
               tb.t[0].i = 0
            }
            tb.t[last] = nil
            tb.t = tb.t[:last]
            if last > 0 {
               siftdownTimer(tb.t, 0)
            }
            t.i = -1 // mark as removed
         }
         f := t.f
         arg := t.arg
         seq := t.seq
         unlock(&tb.lock)//解锁
         f(arg, seq) //在channel中发送time结构体,唤醒阻塞的协程
         lock(&tb.lock)
      }
      if delta < 0  {
         // No timers left - put goroutine to sleep.
         goparkunlock(&tb.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
         continue
      }// delta小于0说明当前无定时任务,直接进行阻塞进行睡眠
      tb.sleeping = true
      tb.sleepUntil = now + delta
      unlock(&tb.lock)
      notetsleepg(&tb.waitnote, delta)  //睡眠delta时间,唤醒之后就可以执行在堆顶的定时任务了
   }
}

至此,time.Tick函数涉及到的主要功能就讲解结束了,总结一下就是启动定时任务时,会创建一个唯一协程,处理timer,所有的timer都在该协程中处理。

然后,我们再阅读一下sleep的源码实现,核心源码如下:

//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
   *t = timer{} //创建一个定时任务
   t.when = nanotime() + ns //计算定时任务的执行时间点
   t.f = goroutineReady //执行方法
   tb.addtimerLocked(t)  //加入timer堆,并在timer定时任务执行协程中等待被执行
   goparkunlock(&tb.lock, "sleep", traceEvGoSleep, 2) //睡眠,等待定时任务协程通知唤醒
}

读了sleep的核心代码之后,是不是突然发现和Tick函数的内容很类似,都创建了timer,并加入了定时任务处理协程。神奇之处就在于,实际上这两个函数产生的timer都放入了同一个timer堆,都在定时任务处理协程中等待被处理。

优劣性对比,使用建议

现在我们知道了,Tick,Sleep,包括time.After函数,都使用的timer结构体,都会被放在同一个协程中统一处理,这样看起来使用Tick,Sleep并没有什么区别。

实际上是有区别的,Sleep是使用睡眠完成定时任务,需要被调度唤醒。Tick函数是使用channel阻塞当前协程,完成定时任务的执行。当前并不清楚golang 阻塞和睡眠对资源的消耗会有什么区别,这方面不能给出建议。

但是使用channel阻塞协程完成定时任务比较灵活,可以结合select设置超时时间以及默认执行方法,而且可以设置timer的主动关闭,以及不需要每次都生成一个timer(这方面节省系统内存,垃圾收回也需要时间)。

所以,建议使用time.Tick完成定时任务。

补充:Golang 定时器timer和ticker

两种类型的定时器:ticker和timer。两者有什么区别呢?请看如下代码:

ticker

package main
import (
        "fmt"
        "time"
)
func main() {
        d := time.Duration(time.Second*2)
        t := time.NewTicker(d)
        defer t.Stop()
        for {
                <- t.C
                fmt.Println("timeout...")
        }
}

output:

timeout…

timeout…

timeout…

解析

ticker只要定义完成,从此刻开始计时,不需要任何其他的操作,每隔固定时间都会触发。

timer

package main
import (
        "fmt"
        "time"
)
func main() {
        d := time.Duration(time.Second*2)
        t := time.NewTimer(d)
        defer t.Stop()
        for {
                <- t.C
                fmt.Println("timeout...")
  // need reset
  t.Reset(time.Second*2)
        }
}

output:

timeout…

timeout…

timeout…

解析

使用timer定时器,超时后需要重置,才能继续触发。

ticker 例子展示

package main
import (
        "fmt"
        "time"
)
func main() {
        t := time.NewTicker(3*time.Second)
        defer t.Stop()
        fmt.Println(time.Now())
        time.Sleep(4*time.Second)
        for {
                select {
                case <-t.C:
                        fmt.Println(time.Now())
                }
        }
}

output:

2018-04-02 19:08:22.2797 +0800 CST

2018-04-02 19:08:26.3087 +0800 CST

2018-04-02 19:08:28.2797 +0800 CST

2018-04-02 19:08:31.2797 +0800 CST

2018-04-02 19:08:34.2797 +0800 CST

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。如有错误或未考虑完全的地方,望不吝赐教。

Golang 相关文章推荐
Go语言使用select{}阻塞main函数介绍
Apr 25 Golang
go原生库的中bytes.Buffer用法
Apr 25 Golang
golang中切片copy复制和等号复制的区别介绍
Apr 27 Golang
goland 清除所有的默认设置操作
Apr 28 Golang
基于Go Int转string几种方式性能测试
Apr 28 Golang
Go 实现英尺和米的简单单位换算方式
Apr 29 Golang
golang goroutine顺序输出方式
Apr 29 Golang
浅谈Golang 切片(slice)扩容机制的原理
Jun 09 Golang
手把手教你导入Go语言第三方库
Aug 04 Golang
Go语言grpc和protobuf
Apr 13 Golang
Go语言安装并操作redis的go-redis库
Apr 14 Golang
golang生成并解析JSON
Apr 14 Golang
golang日志包logger的用法详解
May 05 #Golang
golang elasticsearch Client的使用详解
May 05 #Golang
goland设置颜色和字体的操作
golang协程池模拟实现群发邮件功能
golang 比较浮点数的大小方式
May 02 #Golang
解决Golang中goroutine执行速度的问题
May 02 #Golang
解决golang结构体tag编译错误的问题
May 02 #Golang
You might like
基于php和mysql的简单的dao类实现crud操作功能
2014/01/27 PHP
php的ZipArchive类用法实例
2014/10/20 PHP
php array_values 返回数组的所有值详解及实例
2016/11/12 PHP
php输出含有“#”字符串的方法
2017/01/18 PHP
GWT中复制到剪贴板 js+flash实现复制 兼容性比较好
2010/03/07 Javascript
使用Firebug对js进行断点调试的图文方法
2011/04/02 Javascript
根据表格中的某一列进行排序的javascript代码
2013/11/29 Javascript
js的alert样式如何更改如背景颜色
2014/01/22 Javascript
给js文件传参数(详解)
2014/07/13 Javascript
js使用post 方式打开新窗口
2015/02/26 Javascript
浅谈javascript的分号的使用
2015/05/12 Javascript
分享五个有用的jquery小技巧
2015/10/08 Javascript
AngularJS入门教程之更多模板详解
2016/08/19 Javascript
JS实现鼠标滑过显示边框的菜单效果
2016/09/21 Javascript
微信小程序input框中加入小图标的实现方法
2018/06/19 Javascript
vue2中,根据list的id进入对应的详情页并修改title方法
2018/08/24 Javascript
vue-cli3+ts+webpack实现多入口多出口功能
2019/05/30 Javascript
微信小程序获取用户信息及手机号(后端TP5.0)
2019/09/12 Javascript
一篇文章带你搞懂Vue虚拟Dom与diff算法
2020/08/25 Javascript
vue 验证两次输入的密码是否一致的方法示例
2020/09/29 Javascript
javascript实现时钟动画
2020/12/03 Javascript
简介Python中用于处理字符串的center()方法
2015/05/18 Python
Python3访问并下载网页内容的方法
2015/07/28 Python
Python缩进和冒号详解
2016/06/01 Python
Python随机读取文件实现实例
2017/05/25 Python
Python日期时间模块datetime详解与Python 日期时间的比较,计算实例代码
2018/09/14 Python
Python 点击指定位置验证码破解的实现代码
2019/09/11 Python
python2和python3应该学哪个(python3.6与python3.7的选择)
2019/10/01 Python
css3发光搜索表单分享
2014/04/11 HTML / CSS
查找廉价航班和发现新目的地:Kiwi.com
2019/02/25 全球购物
英国123鲜花网站:123 Flowers
2019/07/07 全球购物
查摆问题自我剖析材料
2014/08/18 职场文书
社区党建工作汇报材料
2014/10/27 职场文书
立案决定书范文
2015/06/24 职场文书
大学学生会竞选稿
2015/11/19 职场文书
详解PHP服务器如何在有限的资源里最大提升并发能力
2021/05/25 PHP