go扩展ticker实现优雅起停

ticker源码分析

关键源码

ticker核心源码

package time

import "errors"

type Ticker struct {
    
    
	C <-chan Time 
	r runtimeTimer
}

func NewTicker(d Duration) *Ticker {
    
    
	if d <= 0 {
    
    
		panic(errors.New("non-positive interval for NewTicker"))
	}
	
	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
}

func (t *Ticker) Stop() {
    
    
	stopTimer(&t.r)
}

startTimer stopTimer sentTime 核心源码

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:
	}
}

// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
    
    
	if raceenabled {
    
    
		racerelease(unsafe.Pointer(t))
	}
	addtimer(t)
}

// stopTimer removes t from the timer heap if it is there.
// It returns true if t was removed, false if t wasn't even there.
//go:linkname stopTimer time.stopTimer
func stopTimer(t *timer) bool {
    
    
	return deltimer(t)
}


func deltimer(t *timer) bool {
    
    
	if t.tb == nil {
    
    
		//若创建了runtimeTimer未加入调度池为false
		return false
	}

	tb := t.tb
    
    //移除runtimeTimer 
	lock(&tb.lock)
	removed, ok := tb.deltimerLocked(t)
	unlock(&tb.lock)
	if !ok {
    
    
		badTimer()
	}
	return removed
}

源码解读

分析

ticker通过startTimer用于在timeproc加入一个调度任务
ticker通过stopTimer用于在timeproc加入去掉调度任务
ticker通过sentTime接收timeproc的调度信号,移除调度任务后并不会关闭ticker通道
ticker起停逻辑为创建并加入调度任务池/从调度任务池删除

注意事项

  1. ticker必须关闭 ,即底层将runtimeTimer移除系统定时任务池,否则ticker的runtimeTimer持续运行,无法回收。
  2. ticker关闭后通道C不能关闭,且使用select且会造成阻塞,即ticker只需要将runtimeTimer移除定时任务池,通道C并没有关闭,使用select时将会阻塞在<-C。不关闭C的原因是底层timerporc sendtime概率性出现给关闭的通道发送。

ticker优化关闭思路

组合形式拓展ticker
通过另外通道来打断select阻塞

MyTicker代码

代码

package main

import (
	"fmt"
	"sync"
	"time"
)

const (
	YYYYMMDDHHMISS = "2006-01-02 15:04:05"
)

type MyTicker struct {
    
    
	*time.Ticker               //扩展定时器
	interval     time.Duration //定时周期
	fn           func()        //回调
	chn          chan bool     //关闭信号
	status       bool          //状态      true表示启动 false表示非启动
}

//设置状态值
func (m *MyTicker) setStatus(status bool) {
    
    
	m.status = status

}

//获取状态值
func (m *MyTicker) getStatus() bool {
    
    
	return m.status
}

//NewTicker interval秒级周期 fn回调函数
func NewTicker(interval int64, fn func()) *MyTicker {
    
    
	m := &MyTicker{
    
    
		interval: time.Duration(interval) * time.Second,
		fn:       fn,
		chn:      make(chan bool),
	}
	return m
}

//Stop 关闭定时器
func (m *MyTicker) Stop() {
    
    
	fmt.Println(time.Now().Format(YYYYMMDDHHMISS), "关闭定时器...")       //打印
	if !m.getStatus() {
    
    
		fmt.Println(time.Now().Format(YYYYMMDDHHMISS), "定时已经关闭")    //打印
		return
	}

    //发送关闭信号
	m.chn <- true
}

//Stop 启动定时器
func (m *MyTicker) Start() {
    
    
	fmt.Println(time.Now().Format(YYYYMMDDHHMISS), "启动定时器...")      //打印
	if m.getStatus() {
    
    
		fmt.Println(time.Now().Format(YYYYMMDDHHMISS), "定时已经开启")   //打印
		return
	}

    //启动携程监听timerporc调度 tiker 以及 自定义关闭信号
	go func() {
    
    
	    //启动 ticker
		m.Ticker = time.NewTicker(m.interval)
		m.setStatus(true)                    
		fmt.Println(time.Now().Format(YYYYMMDDHHMISS), "定时启动")      //打印
        
        //跳出for循环时 关闭ticker
		defer m.Ticker.Stop()
		defer m.setStatus(false)
		defer fmt.Println(time.Now().Format(YYYYMMDDHHMISS), "定时关闭")//打印
		
		//阻塞监听调度信号
		for {
    
    
			select {
    
    
			case <-m.Ticker.C:
			    //监听ticker 信号 调用任务
				go m.fn()
			case <-m.chn:
			    //监听信号  跳出for 执行defer
				return
			default:

			}
		}
	}()
}




func main() {
    
    
	ticker := NewTicker(1, func() {
    
    
		fmt.Println(time.Now().Format(YYYYMMDDHHMISS), "定时器执行")
	})

	ticker.Stop()                 //检测 stop未启动ticker
	time.Sleep(10 * time.Second)
	ticker.Start()                //检测 start
	time.Sleep(10 * time.Second)
	ticker.Start()                //检测 start已启动ticker
	time.Sleep(10 * time.Second)
	ticker.Stop()                 //检测 stop
	time.Sleep(10 * time.Second)
	ticker.Start()                //检测 重启开启ticker
	time.Sleep(10 * time.Second)
	ticker.Stop()                 //检测 重新关闭ticker
	time.Sleep(10 * time.Second)
	select {
    
    }
}

代码效果

2020-07-18 22:37:38 关闭定时器...
2020-07-18 22:37:38 定时已经关闭
2020-07-18 22:37:48 启动定时器...
2020-07-18 22:37:48 定时启动
2020-07-18 22:37:49 定时器执行
2020-07-18 22:37:50 定时器执行
2020-07-18 22:37:51 定时器执行
2020-07-18 22:37:52 定时器执行
2020-07-18 22:37:53 定时器执行
2020-07-18 22:37:54 定时器执行
2020-07-18 22:37:55 定时器执行
2020-07-18 22:37:56 定时器执行
2020-07-18 22:37:57 定时器执行
2020-07-18 22:37:58 启动定时器...
2020-07-18 22:37:58 定时已经开启
2020-07-18 22:37:58 定时器执行
2020-07-18 22:37:59 定时器执行
2020-07-18 22:38:00 定时器执行
2020-07-18 22:38:01 定时器执行
2020-07-18 22:38:02 定时器执行
2020-07-18 22:38:03 定时器执行
2020-07-18 22:38:04 定时器执行
2020-07-18 22:38:05 定时器执行
2020-07-18 22:38:06 定时器执行
2020-07-18 22:38:07 定时器执行
2020-07-18 22:38:08 定时器执行
2020-07-18 22:38:08 关闭定时器...
2020-07-18 22:37:48 定时关闭      //defer导致 定时启动与定时关闭时间一致
2020-07-18 22:38:18 启动定时器...
2020-07-18 22:38:18 定时启动
2020-07-18 22:38:19 定时器执行
2020-07-18 22:38:20 定时器执行
2020-07-18 22:38:21 定时器执行
2020-07-18 22:38:22 定时器执行
2020-07-18 22:38:23 定时器执行
2020-07-18 22:38:24 定时器执行
2020-07-18 22:38:25 定时器执行
2020-07-18 22:38:26 定时器执行
2020-07-18 22:38:27 定时器执行
2020-07-18 22:38:28 关闭定时器...
2020-07-18 22:38:18 定时关闭      //defer导致 定时启动与定时关闭时间一致
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:// 说明go携程全部回收
main.main()
        /Users/zyj/go/src/zyj.com/ticker/main.go:104 +0x15f

参考
[1]: https://my.oschina.net/renhc/blog/3027376/print
[2]: http://xiaorui.cc/archives/6109

猜你喜欢

转载自blog.csdn.net/qq_22211217/article/details/107431258