zoukankan      html  css  js  c++  java
  • Go中的定时器(timer/ticker)

    前言

    go中的定时器包含了两种,一种是一次性的定时器Timer,另外一种是周期性的定时器Ticker。

    Timer

    先看一下Timer是怎么使用的。Timer通常有两种使用方式,一种是显式创建一个定时器,一个是使用匿名定时器:

    func main() {
    	modeOne()
    	moddTwo()
    }
    
    func modeOne() {
    	timer := time.NewTimer(time.Second * 5)
    	<- timer.C
    	fmt.Println("mode one: Time out!")
    }
    
    func moddTwo() {
    	select {
    	case <-time.After(time.Second * 5):
    		fmt.Println("mode two: Time out!")
    	}
    
    } 

    开始的时候可能很迷,为什么模式2就可以作为定时器了呢。了解定时器的结构就很清楚了。下面是定时器的结构体定义:

    type Timer struct {
    	C <-chan Time  // 抛出来的channel,给上层系统使用,实现定时
    	r runtimeTimer  // 给系统管理使用的定时器,系统通过该字段确定定时器是否到时,如果到时,调用对应的函数向C中推送当前时间。
    } 

    方式二中的定时器使用方式是通过After函数构造了一个匿名定时器,并抛出来管道C。总之,两种方式大同小异,都是通过管道C来实现的,定时到了以后,C中有值,则进行相应的操作。

    如果需要停止定时器,可以调用Stop方法,该方法把runtimeTimer从堆中删除。时间到了以后想要调用某个函数,可以直接使用time.AfterFunc方法。

    那么定时器是如何实现的呢?首先看一下定时器的构造:

    func NewTimer(d Duration) *Timer {
    	c := make(chan Time, 1)
    	t := &Timer{
    		C: c,  // 信道
    		r: runtimeTimer{
    			when: when(d),  // 触发时间
    			f:    sendTime, // 时间到了之后的调用函数
    			arg:  c,        // 调用sendTime时的入参
    		},
    	}
    	startTimer(&t.r)  // 把定时器的r字段放入由定时器维护协程维护的堆中
    	return t
    } 

    从上面的构造函数中可以大概看出定时器的工作流程,这里面最重要的是runtimeTimer。构造定时器的时候会把runtimeTimer放入由定时器维护协程维护的堆中,当时间到了之后,维护协程把r从堆中移除,并调用r的sendTime函数,sendTime的入参是定时器的信道C。可以推断,sendTime中执行的逻辑应该是向信道C中推送时间,通知上游系统时间到了,而事实正是如此:

    func sendTime(c interface{}, seq uintptr) {
    	// Non-blocking send of time on c.
    	// Used in NewTimer, it cannot block anyway (buffer).
    	// Used in NewTicker, dropping sends on the floor is
    	// the desired behavior when the reader gets behind,
    	// because the sends are periodic.
    	select {
    	case c.(chan Time) <- Now():  //时间到了之后把当前时间放入信道中
    	default:
    	}
    } 

    可能你会有疑问,为什么这里要用到select,直接往c中放值不久好了吗,而且default分支有什么作用?这里就是定时器设计巧妙的地方,前面讲到go中的定时器包含了一次性定时器和周期定时器,而sendTime是两种定时器共用的。其实Ticker和Timer基本上没有什么差别,实现原理是一样的,结构体字段也是一样的,至少runtimeTimer在构造的时候传入的参数有细微的差别。在Ticker时间到了之后,由于不确定信道C中的内容是否被取走,所以为了sendTime不阻塞,这个时候会走default分支,也就是会丢失一个信号。


    Ticker

    先看一个Ticker的使用示例:

    func main() {
    	tickerDemo()
    }
    
    func tickerDemo() {
    	ticker := time.NewTicker(time.Second)
    	defer ticker.Stop()
    	for range ticker.C {
    		fmt.Println("Time Out!")
    	}
    }
    

      

    Ticker结构体的定义和构造函数如下所示,可以看到与Timer基本一致:

    type Ticker struct {
    	C <-chan Time // The channel on which the ticks are delivered.
    	r runtimeTimer
    }
    
    func NewTicker(d Duration) *Ticker {
    	if d <= 0 {
    		panic(errors.New("non-positive interval for NewTicker"))
    	}
    	// Give the channel a 1-element time buffer.
    	// If the client falls behind while reading, we drop ticks
    	// on the floor until the client catches up.
    	c := make(chan Time, 1)
    	t := &Ticker{
    		C: c,
    		r: runtimeTimer{
    			when:   when(d),
    			period: int64(d),  // 与一次性定时器不一样的地方,这个参数决定了定时器是周期的
    			f:      sendTime,
    			arg:    c,
    		},
    	}
    	startTimer(&t.r)
    	return t
    } 

    周期性定时器到期了之后同样是执行sendTime方法,这个上面已经描述过了。细心的你肯定注意到了,在tickerDemo中有一个defer去停止ticker,为什么要这么做呢?前面分析的时候讲到,创建定时器就是把定时器的runtimeTimer放到由维护协程维护的堆中,一次性定时器到期后,会从堆中删除,如果没有到期则调用Stop方法实现删除。但是,周期性定时器是不会执行删除动作的,所以如果项目里面持续创建多个周期性定时器并没有stop的话,会导致堆越来越大,从而引起资源泄露。

    参考:任洪彩.《GO专家编程》


    Shopee(虾皮)内推点击此处,岗位多多地,薪资高高地



    转载请注明出处


  • 相关阅读:
    cpp 模版函数
    叉积
    利用scrollTop 制作图片无缝滚动
    事件绑定和时间取消
    闭包写法
    增加类,删除类,查找类
    获取元素到页面上的位置
    在IE8中如何通过javascripts改变<style />中的内容?
    有关app的一些小知识
    获取页面高宽知识
  • 原文地址:https://www.cnblogs.com/zhangcaiwang/p/15130774.html
Copyright © 2011-2022 走看看