促使我寫這篇文章主要是在寫一個關於虛擬貨幣賬戶監控的項目時使用 Ticker 的問題。 Ticker 的問題 如果用過 Ticker 的朋友會知道,創建 Ticker 後並不會馬上執行,而是會等待一個時間 d,這就是創建時的間隔時間。如果間隔時間很短這基本上不會有太大問題,但是如果對首次執行時間有要 ...
促使我寫這篇文章主要是在寫一個關於虛擬貨幣賬戶監控的項目時使用 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。
最後的最後
說說我自己,從2月底從好未來辭職(在一家公司呆了5年越來越感覺自己呆在舒適區不想出來,但還是希望能有更多的提升所以自己提出了辭職,我在部門基本屬於是什麼技術都願意瞭解也會深入調研得到成本和收益的關係,人際關係在部門也是作為核心紐帶的角色。),到斷斷續續找工作,因為大專學歷問題,期間只接了一些小公司的 offer 但是最後都拒絕了,自己也好好評估了一下這幾個公司給到的職位和工作內容並不太符合我在接下來5年對自己職業規劃。所以開始自己寫開源項目,然後再找找志同道合的朋友,看有沒有合適的機會一起共事。如果你對我感興趣或者想和我聊聊可以加我微信 ganymede-nil和下載我的簡歷,註明加我的理由,防止被我當作營銷人員