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语言map与string的相互转换的实现
Apr 07 Golang
golang 在windows中设置环境变量的操作
Apr 29 Golang
解决goland 导入项目后import里的包报红问题
May 06 Golang
Go语言空白表示符_的实例用法
Jul 04 Golang
golang中的struct操作
Nov 11 Golang
Go语言读取txt文档的操作方法
Jan 22 Golang
深入理解go缓存库freecache的使用
Feb 15 Golang
Golang 并发下的问题定位及解决方案
Mar 16 Golang
Golang jwt身份认证
Apr 20 Golang
Golang 实现 WebSockets 之创建 WebSockets
Apr 24 Golang
Go微服务项目配置文件的定义和读取示例详解
Jun 21 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 Curl出现403错误的解决办法
2014/05/29 PHP
PHP获取Exif缩略图的方法
2015/07/13 PHP
PHP数据库操作Helper类完整实例
2016/05/11 PHP
jquery tools 系列 scrollable(2)
2009/09/06 Javascript
在html页面上拖放移动标签
2010/01/08 Javascript
基于jquery的兼容各种浏览器的iframe自适应高度的脚本
2010/08/13 Javascript
11个用于提高排版水平的基于jquery的文字效果插件
2012/09/14 Javascript
setTimeout()递归调用不加引号出错的解决方法
2014/09/05 Javascript
nodejs中的fiber(纤程)库详解
2015/03/24 NodeJs
jquery 实时监听输入框值变化的完美方法(必看)
2017/01/26 Javascript
vue 2.x 中axios 封装的get 和post方法
2018/02/28 Javascript
微信小程序的授权实现过程解析
2019/08/02 Javascript
基于vue-cli3+typescript的tsx开发模板搭建过程分享
2020/02/28 Javascript
Python操作Mysql实例代码教程在线版(查询手册)
2013/02/18 Python
详解python中的装饰器
2018/07/10 Python
Python字符串、整数、和浮点型数相互转换实例
2018/08/04 Python
Python发送邮件测试报告操作实例详解
2018/12/08 Python
详解Python 定时框架 Apscheduler原理及安装过程
2019/06/14 Python
Pyqt清空某一个QTreeewidgetItem下的所有分支方法
2019/06/17 Python
pyqt5 键盘监听按下enter 就登陆的实例
2019/06/25 Python
Python流程控制 while循环实现解析
2019/09/02 Python
python3中利用filter函数输出小于某个数的所有回文数实例
2019/11/24 Python
Python3 shelve对象持久存储原理详解
2020/03/23 Python
python名片管理系统开发
2020/06/18 Python
基于CentOS搭建Python Django环境过程解析
2020/08/24 Python
python 第三方库paramiko的常用方式
2021/02/20 Python
英国最大的正宗复古足球衫制造商和零售商:TOFFS
2018/06/21 全球购物
中东奢侈品购物网站:Ounass
2020/09/02 全球购物
行政总监岗位职责
2013/12/05 职场文书
骨干教师培训感言
2014/01/16 职场文书
幼儿园元旦活动感言
2014/03/02 职场文书
入党积极分子自我鉴定范文
2014/03/25 职场文书
小学重阳节活动总结
2015/03/24 职场文书
喜迎建国70周年:有关爱国的名言名句
2019/09/24 职场文书
【海涛DOTA】D-cup邀请赛NV.cn vs DT.Love
2022/04/01 DOTA
Redis实现分布式锁的五种方法详解
2022/06/14 Redis