有时我们想在自己的服务里单独弄一个定时器,但是又不想让定时器的定时任务成为主线程,而是作为 http 服务或者 rpc 服务的一个子线程来执行任务。

一、定时器 NewTicker

1、第一种写法

package main

import (
    "time"
    "fmt"
)

func printDemo() {
  fmt.Println("demo........")
}


// 初始化 demo 定时器
func InitDemoScheduler()  {
    // 每 5 秒钟时执行一次
    ticker := time.NewTicker(5 * time.Second) // 创建一个定时器
    go func() { // 用新协程去执行定时任务
        defer func() {
		if r := recover(); r != nil {
			logs.Error("定时器发生错误,%v", r)
		}
		ticker.Stop() // 意外退出时关闭定时器
	}()
        printDemo()   // 协程启动时启动一次,之后每 5 秒执行一次,如果没有这行,只有等到协程启动后的第 5 秒才会第一次执行任务
        for  { // 用上一个死循环,不停地执行,否则只会执行一次
            select { 
                case <- ticker.C: // 时间到了就会触发这个分支的执行,其实时间到了定时器会往ticker.C 这个 channel 中写一条数据,随后被 select 捕捉到channel中有数据可读,就读取channel数据,执行相应分支的语句
                    printDemo()   
            }
        }
    }()
}

func main(){

  // 初始化定时器,每 5s 会打印一个「demo........」
  InitDemoScheduler()
  
  // 等待,避免主线程退出,实际应用时这里可以时启动 http 服务器的监听动作,或者启动 rpc 服务的监听动作,所以不需要 sleep
  time.sleep(100*time.Second)
}

case <- ticker.Cticker.C 

还有个需要注意的点是:如果需要在协程启动时执行一次任务,需要在在 for 循环之前额外执行一次,这样在协程启动时会启动一次,之后每 5 秒执行一次,如果没有这行,只有等到协程启动后的第 5 秒才会第一次执行任务


2、另一种写法

发现一种新的写法,这种写法没有用 select 语法,而是通过 for 循环从 channel 中取数,理论上应该也可以。但是没实验过,不知道行不行,

// 初始化 demo 定时器
func InitDemoScheduler()  {
	// 每 5 秒钟时执行一次
	ticker := time.NewTicker(5 * time.Second) // 创建一个定时器
	go func() { // 用新协程去执行定时任务
		defer func() {
			if r := recover(); r != nil {
				logs.Error("定时器发生错误,%v", r)
			}
			ticker.Stop() // 意外退出时关闭定时器
		}()
                printDemo()   // 协程启动时启动一次,之后每 5 秒执行一次,如果没有这行,只有等到协程启动后的第 5 秒才会第一次执行任务
		for _ = range ticker.C {
			printDemo()
		}
	}()
}

3、可随时退出的定时任务

上面的两种写法,定时器在创建完成后,协程永远无法退出,如果你想提前退出,可以使用一个无缓冲泳道,对外提供一个往该泳道发送消息的函数,定时任务进程在监听定时器的同时也监听这个无缓冲泳道,如果监听到无缓冲泳道的消息,则立刻 return 终止协程,也就终止了定时任务。

package main

import (
    "time"
    "fmt"
)

func printDemo() {
  fmt.Println("demo........")
}

// 一个无缓冲泳道
var stopFlag = make(chan bool)

// 对外提供一个往泳道写消息的函数,如果想关闭定时任务,调用该函数即可。
func CloseDemoScheduler()  {
	stopFlag <- false
}


// 初始化 demo 定时器
func InitDemoScheduler()  {
	// 每 5 秒钟时执行一次
	ticker := time.NewTicker(5 * time.Second) // 创建一个定时器
	go func() { // 用新协程去执行定时任务
		defer func() {
			if r := recover(); r != nil {
				logs.Error("定时器发生错误,%v", r)
			}
			ticker.Stop() // 意外退出时关闭定时器
		}()
		printDemo()   // 协程启动时启动一次,之后每 5 秒执行一次,如果没有这行,只有等到协程启动后的第 5 秒才会第一次执行任务
		for  { // 用上一个死循环,不停地执行,否则只会执行一次
			select {
			case <- ticker.C: // 时间到了就会触发这个分支的执行,其实时间到了定时器会往ticker.C 这个 channel 中写一条数据,随后被 select 捕捉到channel中有数据可读,就读取channel数据,执行相应分支的语句
				printDemo()
			case <- stopFlag: // 定时任务进程在监听定时器的同时也监听这个无缓冲泳道,如果监听到无缓冲泳道的消息,则立刻 return 终止协程,也就终止了定时任务。
				return
			}
		}
	}()
}

func main(){

  // 初始化定时器,每 5s 会打印一个「demo........」
  InitDemoScheduler()
  
  // 等待,避免主线程退出,实际应用时这里可以时启动 http 服务器的监听动作,或者启动 rpc 服务的监听动作,所以不需要 sleep
  time.sleep(100*time.Second)
}

二、另一种 定时器 Ticker

这种定时器 除了创建定时器调用的是 time.Tick 而不是 time.NewTicker 外,用法跟 NewTicker 完全相同,区别是 Ticker 无法被关闭停止,所以也不需要在 defer 中关闭 Ticker

tick := time.Tick(time.Second)