欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文詳解Golang?定時任務庫?gron?設計和原理

 更新時間:2022年08月18日 08:40:33   作者:ag9920???????  
這篇文章主要介紹了一文詳解Golang?定時任務庫?gron?設計和原理,gron是一個比較小巧、靈活的定時任務庫,可以執(zhí)行定時的、周期性的任務。gron提供簡潔的、并發(fā)安全的接口

 cron 簡介

在 Unix-like 操作系統(tǒng)中,有一個大家都很熟悉的 cli 工具,它能夠來處理定時任務,周期性任務,這就是: cron。 你只需要簡單的語法控制就能實現(xiàn)任意【定時】的語義。用法上可以參考一下這個 Crontab Guru Editor,做的非常精巧。

簡單說,每一個位都代表了一個時間維度,* 代表全集,所以,上面的語義是:在每天早上的4點05分觸發(fā)任務。

但 cron 畢竟只是一個操作系統(tǒng)級別的工具,如果定時任務失敗了,或者壓根沒啟動,cron 是沒法提醒開發(fā)者這一點的。并且,cron 和 正則表達式都有一種魔力,不知道大家是否感同身受,這里引用同事的一句名言:

這世界上有些語言非常相似: shell腳本, es查詢的那個dsl語言, 定時任務的crontab, 正則表達式. 他們相似就相似在每次要寫的時候基本都得重新現(xiàn)學一遍。

正巧,最近看到了 gron 這個開源項目,它是用 Golang 實現(xiàn)一個并發(fā)安全的定時任務庫。實現(xiàn)非常簡單精巧,代碼量也不多。今天我們就來一起結合源碼看一下,怎樣基于 Golang 的能力做出來一個【定時任務庫】。

gron

Gron provides a clear syntax for writing and deploying cron jobs.

gron 是一個泰國小哥在 2016 年開源的作品,它的特點就在于非常簡單和清晰的語義來定義【定時任務】,你不用再去記 cron 的語法。我們來看下作為使用者怎樣上手。

首先,我們還是一個 go get 安裝依賴:

$ go get github.com/roylee0704/gron

假設我們期望在【時機】到了以后,要做的工作是打印一個字符串,每一個小時執(zhí)行一次,我們就可以這樣:

package main

import (
	"fmt"
	"time"
	"github.com/roylee0704/gron"
)
func main() {
	c := gron.New()
	c.AddFunc(gron.Every(1*time.Hour), func() {
		fmt.Println("runs every hour.")
	})
	c.Start()
}

非常簡單,而且即便是在 c.Start 之后我們依然可以添加新的定時任務進去。支持了很好的擴展性。

定時參數(shù)

注意到我們調(diào)用 gron.New().AddFunc() 時傳入了一個 gron.Every(1*time.Hour)

這里其實你可以傳入任何一個 time.Duration,從而把調(diào)度間隔從 1 小時調(diào)整到 1 分鐘甚至 1 秒。

除此之外,gron 還很貼心地封裝了一個 xtime 包用來把常見的 time.Duration 封裝起來,這里我們開箱即用。

import "github.com/roylee0704/gron/xtime"

gron.Every(1 * xtime.Day)
gron.Every(1 * xtime.Week)

很多時候我們不僅僅某個任務在當天運行,還希望是我們指定的時刻,而不是依賴程序啟動時間,機械地加 24 hour。gron 對此也做了很好的支持:

gron.Every(30 * xtime.Day).At("00:00")
gron.Every(1 * xtime.Week).At("23:59")

我們只需指定 At("hh:mm") 就可以實現(xiàn)在指定時間執(zhí)行。

源碼解析

這一節(jié)我們來看看 gron 的實現(xiàn)原理。

所謂定時任務,其實包含兩個層面:

  • 觸發(fā)器。即我們希望這個任務在什么時間點,什么周期被觸發(fā);
  • 任務。即我們在觸發(fā)之后,希望執(zhí)行的任務,類比到我們上面示例的 fmt.Println。

對這兩個概念的封裝和擴展是一個定時任務庫必須考慮的。

而同時,我們是在 Golang 的協(xié)程上跑程序的,意味著這會是一個長期運行的協(xié)程,否則你即便指定了【一個月后干XXX】這個任務,程序兩天后掛了,也就無法實現(xiàn)你的訴求了。

所以,我們還希望有一個 manager 的角色,來管理我們的一組【定時任務】,如何調(diào)度,什么時候啟動,怎么停止,啟動了以后還想加新任務是否支持。

Cron

在 gron 的體系里,Cron 對象(我們上面通過 gron.New 創(chuàng)建出來的)就是我們的 manager,而底層的一個個【定時任務】則對應到 Cron 對象中的一個個 Entry:

// Cron provides a convenient interface for scheduling job such as to clean-up
// database entry every month.
//
// Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may also be started, stopped and the entries
// may be inspected.
type Cron struct {
	entries []*Entry
	running bool
	add     chan *Entry
	stop    chan struct{}
}

// New instantiates new Cron instant c.
func New() *Cron {
	return &Cron{
		stop: make(chan struct{}),
		add:  make(chan *Entry),
	}
}
  • entries 就是定時任務的核心能力,它記錄了一組【定時任務】;
  • running 用來標識這個 Cron 是否已經(jīng)啟動;
  • add 是一個channel,用來支持在 Cron 啟動后,新增的【定時任務】;
  • stop 同樣是個channel,注意到是空結構體,用來控制 Cron 的停止。這個其實是經(jīng)典寫法了,對日常開發(fā)也有借鑒意義,我們待會兒會好好看一下。

我們觀察到,當調(diào)用 gron.New() 方法后,得到的是一個指向 Cron 對象的指針。此時只是初始化了 stop 和 add 兩個 channel,沒有啟動調(diào)度。

Entry

重頭戲來了,Cron 里面的 []*Entry 其實就代表了一組【定時任務】,每個【定時任務】可以簡化理解為 <觸發(fā)器,任務> 組成的一個 tuple。

// Entry consists of a schedule and the job to be executed on that schedule.
type Entry struct {
	Schedule Schedule
	Job      Job

	// the next time the job will run. This is zero time if Cron has not been
	// started or invalid schedule.
	Next time.Time

	// the last time the job was run. This is zero time if the job has not been
	// run.
	Prev time.Time
}

// Schedule is the interface that wraps the basic Next method.
//
// Next deduces next occurring time based on t and underlying states.
type Schedule interface {
	Next(t time.Time) time.Time
}

// Job is the interface that wraps the basic Run method.
//
// Run executes the underlying func.
type Job interface {
	Run()
}
  • Schedule 代表了一個【觸發(fā)器】,或者說一個定時策略。它只包含一個 Next 方法,接受一個時間點,業(yè)務要返回下一次觸發(fā)調(diào)動的時間點。
  • Job 則是對【任務】的抽象,只需要實現(xiàn)一個 Run 方法,沒有入?yún)⒊鰠ⅰ?/li>

除了這兩個核心依賴外,Entry 結構還包含了【前一次執(zhí)行時間點】和【下一次執(zhí)行時間點】,這個目前可以忽略,只是為了輔助代碼用。

按照時間排序

// byTime is a handy wrapper to chronologically sort entries.
type byTime []*Entry

func (b byTime) Len() int      { return len(b) }
func (b byTime) Swap(i, j int) { b[i], b[j] = b[j], b[i] }

// Less reports `earliest` time i should sort before j.
// zero time is not `earliest` time.
func (b byTime) Less(i, j int) bool {

	if b[i].Next.IsZero() {
		return false
	}
	if b[j].Next.IsZero() {
		return true
	}

	return b[i].Next.Before(b[j].Next)
}

這里是對 Entry 列表的簡單封裝,因為我們可能同時有多個 Entry 需要調(diào)度,處理的順序很重要。這里實現(xiàn)了 sort 的接口, 有了 Len()Swap()Less() 我們就可以用 sort.Sort() 來排序了。

此處的排序策略是按照時間大小。

新增定時任務

我們在示例里面出現(xiàn)過調(diào)用 AddFunc() 來加入一個 gron.Every(xxx) 這樣一個【定時任務】。其實這是給用戶提供的簡單封裝。

// JobFunc is an adapter to allow the use of ordinary functions as gron.Job
// If f is a function with the appropriate signature, JobFunc(f) is a handler
// that calls f.
//
// todo: possibly func with params? maybe not needed.
type JobFunc func()

// Run calls j()
func (j JobFunc) Run() {
	j()
}


// AddFunc registers the Job function for the given Schedule.
func (c *Cron) AddFunc(s Schedule, j func()) {
	c.Add(s, JobFunc(j))
}

// Add appends schedule, job to entries.
//
// if cron instant is not running, adding to entries is trivial.
// otherwise, to prevent data-race, adds through channel.
func (c *Cron) Add(s Schedule, j Job) {

	entry := &Entry{
		Schedule: s,
		Job:      j,
	}

	if !c.running {
		c.entries = append(c.entries, entry)
		return
	}
	c.add <- entry
}

JobFunc 實現(xiàn)了我們上一節(jié)提到的 Job 接口,基于此,我們就可以讓用戶直接傳入一個 func() 就ok,內(nèi)部轉(zhuǎn)成 JobFunc,再利用通用的 Add 方法將其加入到 Cron 中即可。

注意,這里的 Add 方法就是新增定時任務的核心能力了,我們需要觸發(fā)器 Schedule,任務 Job。并以此來構造出一個定時任務 Entry。

若 Cron 實例還沒啟動,加入到 Cron 的 entries 列表里就ok,隨后啟動的時候會處理。但如果已經(jīng)啟動了,就直接往 add 這個 channel 中塞,走額外的新增調(diào)度路徑。

啟動和停止

// Start signals cron instant c to get up and running.
func (c *Cron) Start() {
	c.running = true
	go c.run()
}


// Stop halts cron instant c from running.
func (c *Cron) Stop() {

	if !c.running {
		return
	}
	c.running = false
	c.stop <- struct{}{}
}

我們先 high level 地看一下一個 Cron 的啟動和停止。

  • Start 方法執(zhí)行的時候會先將 running 變量置為 true,用來標識實例已經(jīng)啟動(啟動前后加入的定時任務 Entry 處理策略是不同的,所以這里需要標識),然后啟動一個 goroutine 來實際跑啟動的邏輯。
  • Stop 方法則會將 running 置為 false,然后直接往 stop channel 塞一個空結構體即可。

ok,有了這個心里預期,我們來看看 c.run() 里面干了什么事:

var after = time.After


// run the scheduler...
//
// It needs to be private as it's responsible of synchronizing a critical
// shared state: `running`.
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.
		}
	}
}

重點來了,看看我們是如何把上面 Cron, Entry, Schedule, Job 串起來的。

  • 首先拿到 local 的時間 now;
  • 遍歷所有 Entry,調(diào)用 Next 方法拿到各個【定時任務】下一次運行的時間點;
  • 對所有 Entry 按照時間排序(我們上面提過的 byTime);
  • 拿到第一個要到期的時間點,在 select 里面通過 time.After 來監(jiān)聽。到點了就起動新的 goroutine 跑對應 entry 里的 Job,并回到 for 循環(huán),繼續(xù)重新 sort,再走同樣的流程;
  • 若 add channel 里有新的 Entry 被加進來,就加入到 Cron 的 entries 里,觸發(fā)新的 sort;
  • 若 stop channel 收到了信號,就直接 return,結束執(zhí)行。

整體實現(xiàn)還是非常簡潔的,大家可以感受一下。

Schedule

前面其實我們暫時將觸發(fā)器的復雜性封裝在 Schedule 接口中了,但怎么樣實現(xiàn)一個 Schedule 呢?

尤其是注意,我們還支持 At 操作,也就是指定 Day,和具體的小時,分鐘?;貞浺幌拢?/p>

gron.Every(30 * xtime.Day).At("00:00")
gron.Every(1 * xtime.Week).At("23:59")

這一節(jié)我們就來看看,gron.Every 干了什么事,又是如何支持 At 方法的。

// Every returns a Schedule reoccurs every period p, p must be at least
// time.Second.
func Every(p time.Duration) AtSchedule {

	if p < time.Second {
		p = xtime.Second
	}

	p = p - time.Duration(p.Nanoseconds())%time.Second // truncates up to seconds

	return &periodicSchedule{
		period: p,
	}
}

gron 的 Every 函數(shù)接受一個 time.Duration,返回了一個 AtSchedule 接口。我待會兒會看,這里注意,Every 里面是會把【秒】級以下給截掉。

我們先來看下,最后返回的這個 periodicSchedule 是什么:

type periodicSchedule struct {
	period time.Duration
}

// Next adds time t to underlying period, truncates up to unit of seconds.
func (ps periodicSchedule) Next(t time.Time) time.Time {
	return t.Truncate(time.Second).Add(ps.period)
}

// At returns a schedule which reoccurs every period p, at time t(hh:ss).
//
// Note: At panics when period p is less than xtime.Day, and error hh:ss format.
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,
	}
}

// parse naively tokenises hours and minutes.
//
// returns error when input format was incorrect.
func parse(hhmm string) (hh int, mm int, err error) {

	hh = int(hhmm[0]-'0')*10 + int(hhmm[1]-'0')
	mm = int(hhmm[3]-'0')*10 + int(hhmm[4]-'0')

	if hh < 0 || hh > 24 {
		hh, mm = 0, 0
		err = errors.New("invalid hh format")
	}
	if mm < 0 || mm > 59 {
		hh, mm = 0, 0
		err = errors.New("invalid mm format")
	}

	return
}

可以看到,所謂 periodicSchedule 就是一個【周期性觸發(fā)器】,只維護一個 time.Duration 作為【周期】。

periodicSchedule 實現(xiàn) Next 的方式也很簡單,把秒以下的截掉之后,直接 Add(period),把周期加到當前的 time.Time 上,返回新的時間點。這個大家都能想到。

重點在于,對 At 能力的支持。我們來關注下 func (ps periodicSchedule) At(t string) Schedule 這個方法

  • 若周期連 1 天都不到,不支持 At 能力,因為 At 本質(zhì)是在選定的一天內(nèi),指定小時,分鐘,作為輔助。連一天都不到的周期,是要精準處理的;
  • 將用戶輸入的形如 "23:59" 時間字符串解析出來【小時】和【分鐘】;
  • 構建出一個 atSchedule 對象,包含了【周期時長】,【小時】,【分鐘】。

ok,這一步只是拿到了材料,那具體怎樣處理呢?這個還是得繼續(xù)往下走,看看 atSchedule 結構干了什么:

type atSchedule struct {
	period time.Duration
	hh     int
	mm     int
}

// reset returns new Date based on time instant t, and reconfigure its hh:ss
// according to atSchedule's hh:ss.
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)
}

// Next returns **next** time.
// if t passed its supposed schedule: reset(t), returns reset(t) + period,
// else returns reset(t).
func (as atSchedule) Next(t time.Time) time.Time {
	next := as.reset(t)
	if t.After(next) {
		return next.Add(as.period)
	}
	return next
}

其實只看這個 Next 的實現(xiàn)即可。我們從 periodSchedule 那里獲取了三個屬性。

在調(diào)用 Next 方法時,先做 reset,根據(jù)原有 time.Time 的年,月,日,以及用戶輸入的 At 中的小時,分鐘,來構建出來一個 time.Time 作為新的時間點。

此后判斷是在哪個周期,如果當前周期已經(jīng)過了,那就按照下個周期的時間點返回。

到這里,一切就都清楚了,如果我們不用 At 能力,直接 gron.Every(xxx),那么直接就會調(diào)用

t.Truncate(time.Second).Add(ps.period)

拿到一個新的時間點返回。

而如果我們要用 At 能力,指定當天的小時,分鐘。那就會走到 periodicSchedule.At 這里,解析出【小時】和【分鐘】,最后走 Next 返回 reset 之后的時間點。

這個和 gron.Every 方法返回的 AtSchedule 接口其實是完全對應的:

// AtSchedule extends Schedule by enabling periodic-interval & time-specific setup
type AtSchedule interface {
	At(t string) Schedule
	Schedule
}

直接就有一個 Schedule 可以用,但如果你想針對天級以上的 duration 指定時間,也可以走 At 方法,也會返回一個 Schedule 供我們使用。

擴展性

gron 里面對于所有的依賴也都做成了【依賴接口而不是實現(xiàn)】。Cron 的 Add 函數(shù)的入?yún)⒁彩莾蓚€接口,這里可以隨意替換:func (c *Cron) Add(s Schedule, j Job)。

最核心的兩個實體依賴 Schedule, Job 都可以用你自定義的實現(xiàn)來替換掉。

如實現(xiàn)一個新的 Job:

type Reminder struct {
	Msg string
}

func (r Reminder) Run() {
  fmt.Println(r.Msg)
}

事實上,我們上面提到的 periodicSchedule 以及 atSchedule 就是 Schedule 接口的具體實現(xiàn)。我們也完全可以不用 gron.Every,而是自己寫一套新的 Schedule 實現(xiàn)。只要實現(xiàn) Next(p time.Duration) time.Time 即可。

我們來看一個完整用法案例:

package main

import (
	"fmt"
	"github.com/roylee0704/gron"
	"github.com/roylee0704/gron/xtime"
)
type PrintJob struct{ Msg string }
func (p PrintJob) Run() {
	fmt.Println(p.Msg)
}

func main() {

	var (
		// schedules
		daily     = gron.Every(1 * xtime.Day)
		weekly    = gron.Every(1 * xtime.Week)
		monthly   = gron.Every(30 * xtime.Day)
		yearly    = gron.Every(365 * xtime.Day)

		// contrived jobs
		purgeTask = func() { fmt.Println("purge aged records") }
		printFoo  = printJob{"Foo"}
		printBar  = printJob{"Bar"}
	)

	c := gron.New()

	c.Add(daily.At("12:30"), printFoo)
	c.AddFunc(weekly, func() { fmt.Println("Every week") })
	c.Start()

	// Jobs may also be added to a running Gron
	c.Add(monthly, printBar)
	c.AddFunc(yearly, purgeTask)

	// Stop Gron (running jobs are not halted).
	c.Stop()
}

經(jīng)典寫法-控制退出

這里我們還是要聊一下 Cron 里控制退出的經(jīng)典寫法。我們把其他不相關的部分清理掉,只留下核心代碼:

type Cron struct {
	stop    chan struct{}
}

func (c *Cron) Stop() {
	c.stop <- struct{}{}
}

func (c *Cron) run() {

	for {
		select {
		case <-c.stop:
			return // terminate go-routine.
		}
	}
}

空結構體能夠最大限度節(jié)省內(nèi)存,畢竟我們只是需要一個信號。核心邏輯用 for + select 的配合,這樣當我們需要結束時可以立刻響應。非常經(jīng)典,建議大家日常有需要的時候采用。

結語

gron 整體代碼其實只在 cron.go 和 schedule.go 兩個文件,合起來代碼不過 300 行,非常精巧,基本沒有冗余,擴展性很好,是非常好的入門材料。

不過,作為一個 cron 的替代品,其實 gron 還是有自己的問題的。簡單講就是,如果我重啟了一個EC2實例,那么我的 cron job 其實也還會繼續(xù)執(zhí)行,這是落盤的,操作系統(tǒng)級別的支持。

但如果我執(zhí)行 gron 的進程掛掉了,不好意思,那就完全涼了。你只有重啟,然后再把所有任務加回來才行。而我們既然要用 gron,是很有可能定一個幾天后,幾個星期后,幾個月后這樣的觸發(fā)器的。誰能保證進程一直活著呢?連機子本身都可能重啟。

所以,我們需要一定的機制來保證 gron 任務的可恢復性,將任務落盤,持久化狀態(tài)信息,算是個思考題,這里大家可以考慮一下怎么做。

到此這篇關于一文詳解Golang 定時任務庫 gron 設計和原理的文章就介紹到這了,更多相關Golang   gron內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

  • 一文了解Go語言io.Copy函數(shù)

    一文了解Go語言io.Copy函數(shù)

    這篇文章主要為大家介紹了Go語言io.Copy函數(shù)使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-07-07
  • Go錯誤處理的幾種方式

    Go錯誤處理的幾種方式

    在Go語言中,錯誤處理是一種重要的編程模式,它用于處理可能出現(xiàn)的錯誤或異常情況,本文就來介紹一下Go錯誤處理的幾種方式,感興趣的可以了解一下
    2023-11-11
  • Golang?Makefile示例深入講解使用

    Golang?Makefile示例深入講解使用

    一次偶然的機會,在?github?上看到有人用?Makefile,就嘗試了一下,發(fā)現(xiàn)真的非常合適,Makefile?本身就是用來描述依賴的,可讀性非常好,而且與強大的?shell?結合在一起,基本可以實現(xiàn)任何想要的功能
    2023-01-01
  • 如何使用Go語言獲取當天、昨天、明天、某天0點時間戳以及格式化時間

    如何使用Go語言獲取當天、昨天、明天、某天0點時間戳以及格式化時間

    這篇文章主要給大家介紹了關于如何使用Go語言獲取當天、昨天、明天、某天0點時間戳以及格式化時間的相關資料,格式化時間戳是將時間戳轉(zhuǎn)換為特定的日期和時間格式,文中通過代碼示例介紹的非常詳細,需要的朋友可以參考下
    2023-10-10
  • Golang通脈之流程控制詳情

    Golang通脈之流程控制詳情

    這篇文章主要介紹了Golang通脈之流程控制,流程控制是每種編程語言控制邏輯走向和執(zhí)行次序的重要部分,Go語言中最常用的流程控制有if和for,而switch和goto主要是為了簡化代碼,下面文章將詳細介紹改該內(nèi)容,需要的朋友可以參考一下
    2021-10-10
  • Go疑難雜癥講解之為什么nil不等于nil

    Go疑難雜癥講解之為什么nil不等于nil

    在日常開發(fā)中,可能一不小心就會掉進?Go?語言的某些陷阱里,而本文要介紹的?nil?≠?nil?問題,感興趣的小伙伴可以跟隨小編一起了解一下
    2022-10-10
  • Golang與其他語言不同的九個特性

    Golang與其他語言不同的九個特性

    近來關于對Golang的討論有很多,七牛的幾個大牛們也斷定Go語言在未來將會快速發(fā)展,并且很可能會取代Java成為互聯(lián)網(wǎng)時代最受歡迎的編程語言。本文將帶你了解它不同于其他語言的九個特性
    2021-09-09
  • Go語言標準輸入輸出庫的基本使用教程

    Go語言標準輸入輸出庫的基本使用教程

    輸入輸出在任何一門語言中都必須提供的一個功能,下面這篇文章主要給大家介紹了關于Go語言標準輸入輸出庫的基本使用,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-02-02
  • 多階段構建優(yōu)化Go?程序Docker鏡像

    多階段構建優(yōu)化Go?程序Docker鏡像

    這篇文章主要為大家介紹了多階段構建優(yōu)化Go?程序Docker鏡像,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • 最新評論