https://www.cnblogs.com/jiangz222/p/12345566.htmlhtml
robfig/cron是GO語言中一個定時執行註冊任務的package, 最近我在工程中使用到了它,因爲它的實現優雅且簡單(主要是簡單),因此將源碼過了一遍,記錄和分享在此。git
文檔:http://godoc.org/github.com/robfig/cron,repo: https://github.com/robfig/crongithub
基本玩法golang
Demo代碼以下,先用cron.New()初始化一個實例,而後調用AddFunc(spec string, cmd func()) 註冊你但願調用的func,第一個參數爲調度的時間策略,第二個參數爲到時間後執行的方法。robfig/cron支持很是多樣的時間策略(下面的代碼舉了一些例子),最後經過cron.Start()方法啓動。 數據結構
func TestCronDemo(t *testing.T) { c := cron.New() // 經過AddFunc註冊 c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") }) c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") }) c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") }) c.AddFunc("@every 5m", func() { fmt.Println("every 5m, start 5m fron now") }) // 經過AddJob註冊 // var cJob cronJobDemo // c.AddJob("@every 5s", cJob) // 啓動 c.Start() // 中止 c.Stop() } type cronJobDemo int func (c cronJobDemo) Run() { fmt.Println("5s func trigger") return }
上面代碼中,第九、10行的代碼調用方法AddJob(spec string, cmd Job)也能夠實現AddFunc註冊的功能,Job是interface,須要入參類型實現方法:Run()。實際上,方法AddFunc內部將參數cmd 進行了包裝(wrapper),而後也是調用方法AddJob進行註冊。
若是實際工程中定時執行的邏輯較爲複雜,推薦使用方法AddJob()來註冊,本身寫方法Run(),這樣能夠經過Run所屬的類型來傳遞所需數據,後面介紹都會說成AddJob,等效於AddFunc。app
AddJob後發生了什麼? (主要的數據結構)學習
對於Cron的總體邏輯,最關鍵的兩個數據結構就是struct Entry和Cron。spa
每當你用AddJob註冊一個定時調用策略,就會爲這個策略生成一個惟一的Entry,不難想象,Entry裏會存儲被執行的時間、須要被調度執行的實體Job。code
生成entry後,再將entry放到struct Cron的entry列表裏,Cron的結構裏,主要是一些用來和外部交互的channel,好比經過channel添加、刪除entry等。詳見下面的代碼。htm
// Entry 數據結構,每個被調度實體一個 type Entry struct { // 惟一id,用於查詢和刪除 ID EntryID // 本Entry的調度時間,不是絕對時間,在生成entry時會計算出來 Schedule Schedule // 本entry下次須要執行的絕對時間,會一直被更新 // 被封裝的含義是Job能夠多層嵌套,能夠實現基於須要執行Job的額外處理 // 好比抓取Job異常、若是Job沒有返回下一個時間點的Job是仍是繼續執行仍是delay Next time.Time // 上一次被執行時間,主要用來查詢 Prev time.Time // WrappedJob 是真實執行的Job實體 WrappedJob Job // Job 主要給用戶查詢 Job Job } // Cron 數據結構,爲robfig/cron的運行實體使用的s數據結構 type Cron struct { entries []*Entry // 調度執行實體列表 // chain 用來定義entry裏的warppedJob使用什麼邏輯(e.g. skipIfLastRunning) // 即一個cron裏全部entry只有一個封裝邏輯 chain Chain stop chan struct{} // 中止整個cron的channel add chan *Entry // 增長一個entry的channel remove chan EntryID // 移除一個entry的channel snapshot chan chan []Entry // 獲取entry總體快照的channel running bool // 表明是否已經在執行,是cron爲使用者提供的動態修改entry的接口準備的 logger Logger // 封裝golang的log包 runningMu sync.Mutex // 用來修改運行中的cron數據,好比增長entry,移除entry location *time.Location // 地理位置 parser ScheduleParser // 對時間格式的解析,爲interface, 能夠定製本身的時間規則。 nextID EntryID // entry的全局ID,新增一個entry就加1 jobWaiter sync.WaitGroup // run job時會進行add(1), job 結束會done(),stop整個cron,以此保證全部job都能退出 }
須要注意的是,WrappedJob和chain這兩個成員,這是Cron實現的Job封裝邏輯,目前是解決實際調度Job的異常處理。好比你但願本身的上一個時間點的JobA沒有結束,下一個時間點的JobA就不執行,這個「不執行」的邏輯實現就定義在chain,初始化時經過chain將JobA進行封裝寫入WrappedJob,那麼每次JobA調用前會先執行封裝邏輯,進行判斷。
Start後發生了什麼? (程序的主體)
cron.Start()執行後,cron的後臺程序(方法run())就開始運行了。而它的主體,就是一個定時器的實現和到時後的job運行,加上cron裏的數據維護。
cron的定時器實現是一個簡潔而典型的業務層實現,着重瞭解下,具體的流程圖可見下圖。
它的關鍵和值得學習之處是:
上面的邏輯說完,程序主體已經清晰,除此以外,程序主體裏的定時器監聽和其餘多個channel共用了select-case,這些channel在struct Cron裏能看到,實現了entries的動態添加、刪除、entries快照獲取等功能。代碼結構以下:
將這些操做經過channel讓程序主體來操做,能夠有效的減小互斥鎖的使用,也會引入問題,會致使有的job執行時間不是很是精準,致使某些entry被遺漏:
固然,channel的操做首先是很是簡潔省時的,其次,定時器實現裏,會掃描全部當前時間以前的entries來執行,增長了容錯性
值得稱讚的細節
interface的使用
struct Entry裏的Schedule和Cron裏的ScheduleParser都是interface,意味着咱們是能夠本身定製註冊job時的時間策略的格式的,只要本身實現時間策略的解析和獲取方法就好
這讓我想起了之前看過golang裏何時用interface和struct的討論,我以爲這是個很好的例子:預期對同一個接口有多個實現時就抽象成interface,不知道該不應用就用struct。
wrapper的實現
上面有提到,經過對Job的封裝,cron實現了同一個job屢次調用時的異常處理等,值得之後在實踐中借鑑。
最後是我加了一點註釋的代碼,https://github.com/jiangz222/cron/tree/comments-v3