天天看點

支援首次觸發的 Go Ticker

促使我寫這篇文章主要是在寫一個關于虛拟貨币賬戶監控的項目時使用 Ticker 的問題。

Ticker 的問題

如果用過 Ticker 的朋友會知道,建立 Ticker 後并不會馬上執行,而是會等待一個時間

d

,這就是建立時的間隔時間。如果間隔時間很短這基本上不會有太大問題,但是如果對首次執行時間有要求,就會很麻煩。例如以下這個案例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	ts := time.NewTicker(5 * time.Second)
	fmt.Println("start_time#", time.Now().Unix())
	chanClose := make(chan struct{})
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			select {
			case <-chanClose:
				return
			case <-ts.C:
				fmt.Println("run_time#", time.Now().Unix())
			}
		}
	}()

	go func() {
		time.Sleep(10 * time.Second)
		chanClose <- struct{}{}
		ts.Stop()
	}()
	wg.Wait()
}
           

它将傳回以下内容:

start_time# 1656860176
run_time# 1656860181
run_time# 1656860186
           

為了友善示範我們在事例中設了一個很短的時間,我們可以看到從代碼啟動到真正定時器觸發,代碼等待了5秒,就是

time.NewTicker

建立時我們傳的參數時間。但如果我們把這個時間改成1個小時,我們需要等待1個小時才會真正開始執行。

尋找解決方案

在我的項目中需要定時器馬上執行,是以我通過搜尋搜到了 Go 官方倉庫 Issues 中提到過這個問題的解決方案 “time: create ticker with instant first tick”。我們可以看一下這個事例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	ts := time.NewTicker(5 * time.Second)
	fmt.Println("start_time#", time.Now().Unix())
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		for ; true; <-ts.C {
			fmt.Println("run_time#", time.Now().Unix())
		}
	}()

	go func() {
		time.Sleep(10 * time.Second)
		ts.Stop()
		wg.Done()
	}()
	wg.Wait()
}
           

上述的執行後傳回内容:

start_time# 1656860889
run_time# 1656860889
run_time# 1656860894
           

我們可以看到首次定時器觸發任務的時間變成了程式執行的開始時間!在我們的例子中,這種方式沒有問題,但是我們需要關注退出條件,在這裡是 main goroutine 直接退出。第一個 goroutine 其實直到 main 退出前一直是堵塞狀态。如果你的項目中多次使用這種形式的定時器,每一個都會有一個堵塞的 goroutine,雖然不會對你程式造成 panic,但我還是感覺不是很好。

我的版本

先上代碼:

package ticktock

import (
	"time"
)
// 這個結構體内容是為了相容 Ticker 的使用方式
type tickerStart struct {
	C      chan time.Time
	ticker *time.Ticker
	close  chan struct{}
}

func NewTickerStart(d time.Duration) *tickerStart {
    // 這裡我們建立的 channel 設了一個 buffer,原因是我們需要
    // 在下面 Start 方法中及時推送目前時間而不至于堵塞。
    // 
	c := make(chan time.Time, 1) 
	return &tickerStart{ticker: time.NewTicker(d), C: c, close: make(chan struct{})}
}
// 這是我們核心的方法
func (ts *tickerStart) Start() {
	ts.C <- time.Now() // 首次觸發關鍵
	go func() {
		for {
			select {
			case _, ok := <-ts.close: // 用于關閉這個 goroutine
				if !ok {
					return
				}
			case t := <-ts.ticker.C: 
			// 把go原生定時器 push 的時間推送到我們定義的 time channel 中
				ts.C <- t
			}

		}
	}()
}
// 相容 ticker
func (ts *tickerStart) Reset(d time.Duration) {
	ts.ticker.Reset(d)
}
// 相容 ticker
func (ts *tickerStart) Stop() {
	ts.ticker.Stop()
	close(ts.close)
}

           

使用代碼如下:

package main

import (
	"fmt"
	"sync"
	"time"
	"ticktock"
)

func main() {
	fmt.Println("start_time#", time.Now().Unix())
	chanClose := make(chan struct{})
	tts := ticktock.NewTickerStart(5 * time.Second)
	tts.Start()
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			select {
			case <-chanClose:
				return
			case <-tts.C:
				fmt.Println("run_time#", time.Now().Unix())
			}
		}
	}()

	go func() {
		time.Sleep(10 * time.Second)
		chanClose <- struct{}{}
		tts.Stop()
	}()
	wg.Wait()
}
           

執行傳回内容如下:

start_time# 1656861872
run_time# 1656861872
run_time# 1656861877
           

可以看到,和我們想要的一緻。但和官方給出的不同我們不會堵塞 goroutine 。

最後

這是我在寫虛拟貨币賬戶監控項目中碰到的其中一個問題,我也會在後續的文章中寫一寫我碰到的其他問題。當然這個項目會開源,可以關注我的 github GanymedeNil's github。

最後的最後

順便給自己宣傳一下,如果你對我感興趣或者想和我聊聊可以加我微信 ganymede-nil和下載下傳我的履歷,找工作中,注明加我的理由,防止被我當作營銷人員😊。

go