如何用golang實現一個定時器任務隊列 原 薦

golang中定時器

golang中提供了2種定時器timer和ticker(如果JS很熟悉的話應該會很瞭解),分別是一次性定時器和重複任務定時器。

一般用法:

func main() {
 
    input := make(chan interface{})
 
    //producer - produce the messages
    go func() {
        for i := 0; i < 5; i++ {
            input <- i
        }
        input <- "hello, world"
    }()
 
    t1 := time.NewTimer(time.Second * 5)
    t2 := time.NewTimer(time.Second * 10)
 
    for {
        select {
        //consumer - consume the messages
        case msg := <-input:
            fmt.Println(msg)
 
        case <-t1.C:
            println("5s timer")
            t1.Reset(time.Second * 5)
 
        case <-t2.C:
            println("10s timer")
            t2.Reset(time.Second * 10)
        }
    }
}

源碼觀察

這個C是啥,我們去源碼看看,以timer爲例:

type Timer struct {
	C <-chan Time
	r runtimeTimer
}

原來是一個channel,其實有GO基礎的都知道,GO的運算符當出現的->或者<-的時候,必然是有一端是指channel。按照上面的例子來看,就是阻塞在一個for循環內,等待到了定時器的C從channel出來,當獲取到值的時候,進行想要的操作。

設計我們的定時任務隊列

我的需求

當時我的需求是這樣,我需要接收到客戶端的請求併產生一個定時任務,會在固定時間執行,可能是一次,也可能是多次,也可能到指定時間自動停止,可能當任務終止的時候,我還要能停止掉。

具體我畫了個流程圖,差不多如下,畫圖水平有限,請見諒。

流程圖

定義結構

type OnceCron struct {
	tasks  []*Task          //任務的列隊
	add    chan *Task       //當遭遇到新任務的時候
	remove chan string       //當遭遇到刪除任務的時候
	stop   chan struct{}      //當遇到停止信號的時候
	Logger *log.Logger      //日誌  
}
type Job interface {
	Run()                  //執行接口
}
type Task struct {
        Job     Job            //要執行的任務 
	Uuid    string           //任務標識,刪除時用
	RunTime int64           //執行時間
	Spacing int64           //間隔時間
	EndTime int64           //結束時間
	Number  int             //總共要次數
}

隊列實現

首先,我們要獲得一個隊列任務

func NewCron() *OnceCron 常規操作,爲了節省篇幅,我就不寫出來,具體可以看源碼,貼在了底部。

然後,開始定時器隊列的運行,一般,都會命名爲Start。那麼就有一個問題,我們剛開始啓動程序的時候,這個時候是沒有任務隊列,那豈不是for{ select{}}在等待個毛毛球?所以,我們需要在Start的時候添加一個默認的任務, 我是這麼做的,添加了一個一小時執行一次的重複隊列,防止隊列退出。

func (one *OnceCron) Start() {
	//初始化的時候加入一個一年的長定時器,間隔1小時執行一次
	task := getTaskWithFuncSpacing(3600,  time.Now().Add(time.Hour*24*365).Unix() , func() {
		log.Println("It's a Hour timer!")
	}) //爲了代碼格式markdown 裏面有個括號我改成全角了
	one.tasks = append(one.tasks, task)
	go one.run()  //協成執行 防止主進程被阻塞
}

執行部分應該是重點的,我的理解是,分成三部:

  1. 首先獲得一個最先執行的任務
  2. 然後產生一個定時器,用於執行任務
  3. 進行阻塞判斷,獲取我們要進行的操作
func (one *OnceCron) run() {

	for {
                //第一步 獲取任務
		now := time.Now()   //獲取到當前時間
		task, key := one.GetTask() //獲取最近的一個任務的執行時間
		i64 := task.RunTime - now.Unix() //任務執行和當前時間的差

		var d time.Duration
		if i64 < 0 {   //如果任務時間已過期,將執行時間改成現在並且利馬執行
			one.tasks[key].RunTime = now.Unix()  
			one.doAndReset(key)
                        continue
		} else {  //否則,獲取距離執行開始的間隔時間
			d = time.Unix(task.RunTime, 0).Sub(now)
		}
                //第二步 產生定時器
		timer := time.NewTimer(d)  

		//第三步 捕獲定時器或者其他事件
		for {
			select {	
                        //當定時器到了執行時間時,執行當前任務並關閉定時器
			case <-timer.C:
				one.doAndReset(key)
				if task != nil {
					go task.Job.Run()
					timer.Stop()
				}

			//當外部添加了任務時,關閉當前定時器
			case <-one.add:
				timer.Stop()
			//當外部要刪除一個任務時,刪除ID爲uuidstr的任務
			case uuidstr := <-one.remove:
				one.removeTask(uuidstr)
				timer.Stop()
			//當遇到要關閉整個定時器任務時
			case <-one.stop:
				timer.Stop()
				return
			}

			break
		}
	}
}

後記

這個文章純粹爲筆記分析類的文章,旨在分析我碰到一個需求是如何通過分析過程來產生我們需要的代碼的。

源碼地址:

timing 一個任務隊列

應用地址:

一個應用於谷歌消息推送的轉發中間件

參考源碼:

GOLANG實現crontab功能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章