Go 每日一庫之 gron

簡介

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/Hourgron中的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類型,而gronJobFunc實現了gron.Job接口。是否是與net/http包中的HandleFuncHandle很像。若是注意觀察的話,在不少 Go 語言的代碼中都有此類模式。

一點源碼

gron的源碼只有兩個文件cron.goschedule.gocron.go中實現添加任務和調度的方法,schedule.go中是時間策略相關的代碼。兩個文件算上註釋一共才 260 行!咱們添加的任務在gron內部都是以Entry結構表示的:

type Entry struct {
  Schedule Schedule
  Job      Job
  Next time.Time
  Prev time.Time
}
複製代碼

Next爲下次執行時間,Prev爲上次執行時間,Job是要執行的任務,Schedulegron.Schedule接口類型,調用其Next()可計算出下次執行的時間點。

管理器使用gron.Cron結構表示:

type Cron struct {
  entries []*Entry
  running bool
  add     chan *Entry
  stop    chan struct{}
}
複製代碼

任務的調度在另一個 goroutine 中。若是調度未開始,添加任務可直接appendentries切片中;若是調度已開始(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.
    }
  }
}
複製代碼

執行流程以下:

  1. 調度器剛啓動時,先計算全部任務的下次執行時間;
  2. 而後在一個for循環中,按照執行時間從早到晚排序,取出最近須要執行任務的時間點;
  3. select語句中等待到這個時間點,啓動新的 goroutine 執行到期的任務,每一個任務一個新的 goroutine;
  4. 若是在等待的過程當中,又添加了新的任務(經過通道c.add),計算這個新任務的首次執行時間。跳到步驟 2,由於新添加的任務可能最先執行。

有幾個細節須要注意一下:

  1. 任務到期判斷使用的是本地時間:time.Now().Local()
  2. 若是沒有任務,等待時間設置爲now.AddDate(15, 0, 0),即 15 年,防止 CPU 空轉;
  3. 任務都是在獨立的 goroutine 中執行的;
  4. 經過實現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)
}
複製代碼

atScheduleNext()方法先計算當天該時間點,再加上週期就是下次觸發的時間:

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😄

參考

  1. gron GitHub:github.com/roylee0704/…
  2. Go 每日一庫 GitHub:github.com/darjun/go-d…

個人博客:darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索