有时我们想在自己的服务里单独弄一个定时器,但是又不想让定时器的定时任务成为主线程,而是作为 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)