gron
是一個比較小巧、靈活的定時任務庫,能夠執行定時的、週期性的任務。gron
提供簡潔的、併發安全的接口。咱們先介紹gron
庫的使用,而後簡單分析一下源碼。git
先安裝:github
$ go get github.com/roylee0704/gron
複製代碼
後使用:golang
package main
import (
"fmt"
"sync"
"time"
"github.com/roylee0704/gron"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
c := gron.New()
c.AddFunc(gron.Every(5*time.Second), func() {
fmt.Println("runs every 5 seconds.")
})
c.Start()
wg.Wait()
}
複製代碼
gron
的使用比較簡單:安全
gron.New()
建立一個管理器,這是一個定時任務的管理器;AddFunc()
或Add()
方法向它添加任務,在啓動時添加也是能夠的,見下文分析;Start()
方法啓動它。gron
支持兩種添加任務的方式,一種是使用無參數的函數,另外一種是實現任務接口。上面例子中使用的是前一種方式,實現接口的方式咱們後面會介紹。添加任務時經過gron.Every()
指定週期任務的間隔,上面添加了一個 5s 的週期任務,每隔 5s 輸出一行文字。微信
須要注意的是,咱們使用sync.WaitGroup
保證主 goroutine 不退出。由於c.Start()
中只是啓動了一個 goroutine,若是主 goroutine 退出了,整個程序就中止了。併發
運行程序,每隔 5s 輸出:app
runs every 5 seconds.
runs every 5 seconds.
runs every 5 seconds.
複製代碼
該程序須要按下ctrl + c
中止!函數
gron
接受time.Duration
類型的時間間隔,除了time
包中定義的基礎Second/Minute/Hour
,gron
中的xtime
子包還提供了Day/Week
單位的時間。有一點須要注意,gron
支持的時間精度爲 1s,小於 1s 的間隔是不支持的。除了單位時間間隔,咱們還可使用4m10s
這樣的時間:學習
func main() {
var wg sync.WaitGroup
wg.Add(1)
c := gron.New()
c.AddFunc(gron.Every(1*time.Second), func() {
fmt.Println("runs every second.")
})
c.AddFunc(gron.Every(1*time.Minute), func() {
fmt.Println("runs every minute.")
})
c.AddFunc(gron.Every(1*time.Hour), func() {
fmt.Println("runs every hour.")
})
c.AddFunc(gron.Every(1*xtime.Day), func() {
fmt.Println("runs every day.")
})
c.AddFunc(gron.Every(1*xtime.Week), func() {
fmt.Println("runs every week.")
})
t, _ := time.ParseDuration("4m10s")
c.AddFunc(gron.Every(t), func() {
fmt.Println("runs every 4 minutes 10 seconds.")
})
c.Start()
wg.Wait()
}
複製代碼
經過gron.Every()
設置每隔多長時間執行一次任務。對於大於 1 天的時間間隔,咱們還可使用gron.Every().At()
指定其在某個時間點執行。例以下面的程序,從次日的22:00
開始,每隔一天觸發一次,即天天的22:00
觸發:ui
func main() {
var wg sync.WaitGroup
wg.Add(1)
c := gron.New()
c.AddFunc(gron.Every(1*xtime.Day).At("22:00"), func() {
fmt.Println("runs every second.")
})
c.Start()
wg.Wait()
}
複製代碼
實現自定義任務也很簡單,只須要實現gron.Job
接口便可:
// src/github.com/roylee0704/gron/cron.go
type Job interface {
Run()
}
複製代碼
咱們須要調用調度器的Add()
方法向管理器添加自定義任務:
type GreetingJob struct {
Name string
}
func (g GreetingJob) Run() {
fmt.Println("Hello ", g.Name)
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
g1 := GreetingJob{Name: "dj"}
g2 := GreetingJob{Name: "dajun"}
c := gron.New()
c.Add(gron.Every(5*time.Second), g1)
c.Add(gron.Every(10*time.Second), g2)
c.Start()
wg.Wait()
}
複製代碼
上面咱們編寫了一個GreetingJob
結構,實現gron.Job
接口,而後建立兩個對象g1/g2
,一個 5s 觸發一次,一個 10s 觸發一次。使用自定義任務的方式能夠比較好地處理攜帶狀態的任務,如上面的Name
字段。
實際上,AddFunc()
方法內部也是經過Add()
實現的:
// src/github.com/roylee0704/gron/cron.go
func (c *Cron) AddFunc(s Schedule, j func()) {
c.Add(s, JobFunc(j))
}
type JobFunc func() func (j JobFunc) Run() {
j()
}
複製代碼
在AddFunc()
內部,將傳入的函數轉爲JobFunc
類型,而gron
爲JobFunc
實現了gron.Job
接口。是否是與net/http
包中的HandleFunc
和Handle
很像。若是注意觀察的話,在不少 Go 語言的代碼中都有此類模式。
gron
的源碼只有兩個文件cron.go
和schedule.go
,cron.go
中實現添加任務和調度的方法,schedule.go
中是時間策略相關的代碼。兩個文件算上註釋一共才 260 行!咱們添加的任務在gron
內部都是以Entry
結構表示的:
type Entry struct {
Schedule Schedule
Job Job
Next time.Time
Prev time.Time
}
複製代碼
Next
爲下次執行時間,Prev
爲上次執行時間,Job
是要執行的任務,Schedule
爲gron.Schedule
接口類型,調用其Next()
可計算出下次執行的時間點。
管理器使用gron.Cron
結構表示:
type Cron struct {
entries []*Entry
running bool
add chan *Entry
stop chan struct{}
}
複製代碼
任務的調度在另一個 goroutine 中。若是調度未開始,添加任務可直接append
到entries
切片中;若是調度已開始(Start()
方法已調用),須要向通道add
發送待添加的任務。任務調度的核心邏輯在Run()
方法中:
func (c *Cron) run() {
var effective time.Time
now := time.Now().Local()
// to figure next trig time for entries, referenced from now
for _, e := range c.entries {
e.Next = e.Schedule.Next(now)
}
for {
sort.Sort(byTime(c.entries))
if len(c.entries) > 0 {
effective = c.entries[0].Next
} else {
effective = now.AddDate(15, 0, 0) // to prevent phantom jobs.
}
select {
case now = <-after(effective.Sub(now)):
// entries with same time gets run.
for _, entry := range c.entries {
if entry.Next != effective {
break
}
entry.Prev = now
entry.Next = entry.Schedule.Next(now)
go entry.Job.Run()
}
case e := <-c.add:
e.Next = e.Schedule.Next(time.Now())
c.entries = append(c.entries, e)
case <-c.stop:
return // terminate go-routine.
}
}
}
複製代碼
執行流程以下:
for
循環中,按照執行時間從早到晚排序,取出最近須要執行任務的時間點;select
語句中等待到這個時間點,啓動新的 goroutine 執行到期的任務,每一個任務一個新的 goroutine;c.add
),計算這個新任務的首次執行時間。跳到步驟 2,由於新添加的任務可能最先執行。有幾個細節須要注意一下:
time.Now().Local()
;now.AddDate(15, 0, 0)
,即 15 年,防止 CPU 空轉;sort.Interface
接口能夠實現自定義排序(代碼中的byTime
)。最後,咱們來看一下時間策略的代碼。咱們知道在Entry
結構中存儲了一個gron.Schedule
類型的對象,調用該對象的Next()
方法返回下次執行的時間點:
// src/github.com/roylee0704/gron/schedule.go
type Schedule interface {
Next(t time.Time) time.Time
}
複製代碼
gron
內置實現了兩種Schedule
,一種是periodicSchedule
,即週期觸發,gron.Every()
函數返回的就是這個對象:
// src/github.com/roylee0704/gron/schedule.go
type periodicSchedule struct {
period time.Duration
}
複製代碼
一種是固定時刻的週期觸發,它實際上也是週期觸發,只是固定了時間點:
type atSchedule struct {
period time.Duration
hh int
mm int
}
複製代碼
他們的核心邏輯在Next()
方法中,periodicSchedule
只須要用當前時間加上週期便可獲得下次觸發時間。這裏Truncate()
方法截掉了當前時間中小於 1s 的部分:
func (ps periodicSchedule) Next(t time.Time) time.Time {
return t.Truncate(time.Second).Add(ps.period)
}
複製代碼
atSchedule
的Next()
方法先計算當天該時間點,再加上週期就是下次觸發的時間:
func (as atSchedule) reset(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), as.hh, as.mm, 0, 0, time.UTC)
}
func (as atSchedule) Next(t time.Time) time.Time {
next := as.reset(t)
if t.After(next) {
return next.Add(as.period)
}
return next
}
複製代碼
periodicSchedule
提供了At()
方法能夠轉爲atSchedule
:
func (ps periodicSchedule) At(t string) Schedule {
if ps.period < xtime.Day {
panic("period must be at least in days")
}
// parse t naively
h, m, err := parse(t)
if err != nil {
panic(err.Error())
}
return &atSchedule{
period: ps.period,
hh: h,
mm: m,
}
}
複製代碼
咱們能夠很輕鬆的實現一個自定義的時間策略。例如,咱們要實現一個「指數退避」的時間序列,先等待 1s,而後 2s、4s...
type ExponentialBackOffSchedule struct {
last int
}
func (e *ExponentialBackOffSchedule) Next(t time.Time) time.Time {
interval := time.Duration(math.Pow(2.0, float64(e.last))) * time.Second
e.last += 1
return t.Truncate(time.Second).Add(interval)
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
c := gron.New()
c.AddFunc(&ExponentialBackOffSchedule{}, func() {
fmt.Println(time.Now().Local().Format("2006-01-02 15:04:05"), "hello")
})
c.Start()
wg.Wait()
}
複製代碼
運行結果以下:
2020-04-20 23:47:11 hello
2020-04-20 23:47:13 hello
2020-04-20 23:47:17 hello
2020-04-20 23:47:25 hello
複製代碼
第二次輸出與第一次相差 2s,第三次與第二次相差 4s,第4次與第三次相差 8s,完美!
本文介紹了gron
這個小巧的定時任務庫,如何使用,如何自定義任務和時間策略,順帶分析了一下源碼。gron
源碼實現很是簡潔,很是推薦閱讀!
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
個人博客:darjun.github.io
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~