有时我们想在自己的服务里单独弄一个定时器,但是又不想让定时器的定时任务成为主线程,而是作为 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.C
: 间到了定时器会往ticker.C
这个 channel 中写一条数据,随后被 select 捕捉到 channel 中有数据可读,就读取 channel 数据,并执行相应分支的语句,select 语法可以参考:golang中的select详解(转)
还有个需要注意的点是:如果需要在协程启动时执行一次任务,需要在在 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)