這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
問題描述
現在我們建立了一個定時器,能定時的去做某件事,並且在執行時間逾時的時候,能把這個定時器關掉。例如需要收集一周的日誌,建立一個定時任務去收集日誌,每5秒鐘執行一次,一周的時間過後需要停掉這個定時任務。
標準庫Ticker
標準庫提供裡的Ticker類,主要功能是定時重複的去做某件事情,如果沒有設定逾時,它會一直執行下去。常見的寫法如下:
t := time.NewTicker(3 * time.Second)timeout := time.After(10 * time.Second)go func() { for { <-t.C ... } }()<-timeout...
注意到這個Ticker對象是無法關閉的
,好的,你可能會發現Ticker類提供了Stop方法。但是我們看看如果你這樣去關閉t的話,會出現什麼情況。
package mainimport ( "fmt" "time")func DoTickerWork(res chan interface{}, timeout <-chan time.Time) { t := time.NewTicker(3 * time.Second) go func() { defer close(res) i := 1 for { <-t.C fmt.Printf("start %d th worker\n", i) res <- i i++ } }() <-timeout t.Stop() return}func main() { res := make(chan interface{}, 10000) timeout := time.After(10 * time.Second) DoTickerWork(res, timeout) for v := range res { fmt.Println(v) }}
直覺上來看,新起的goroutine在等待的過程中,主線程會把定時器關掉,似乎沒有什麼bug,然而輸出是這樣:
$go run ticker.go start 1 th workerstart 2 th workerstart 3 th worker123fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:main.main() /home/gepin.zs/go/src/timer/ticker.go:29 +0xadgoroutine 6 [chan receive]:main.DoTickerWork.func1(0xc42006c060, 0xc4200161c0) /home/gepin.zs/go/src/timer/ticker.go:14 +0x8ecreated by main.DoTickerWork /home/gepin.zs/go/src/timer/ticker.go:19 +0x60exit status 2
這說明Ticker對象的stop方法並沒有關掉
這個Ticker的channel,而只是阻止了channel的資料寫入
,所以goroutine的任務依然在進行中,但是<-t.C一直阻塞,出現了deadlock的情況。可能會有人說調用close(t.C)就可以了,但是編譯會報錯:cannot close receive-only channel, 因為t.C是一個唯讀隊列,無法調用close方法。
怎麼解決
不要以為stop就可以關掉Ticker了,我們可以建立一個名字為done的channel,緩衝大小為1,goroutine裡面採用select,然後嘗試擷取timeout,如果能夠取到,說明已經觸發逾時,然後close(done),這個時候任務結束,主線程return。代碼如下:
package mainimport ( "fmt" "time")func DoTickerWork(res chan interface{}, timeout <-chan time.Time) { t := time.NewTicker(3 * time.Second) done := make(chan bool, 1) go func() { defer close(res) i := 1 for { select { case <-t.C: fmt.Printf("start %d th worker\n", i) res <- i i++ case <-timeout: close(done) return } } }() <-done return}func main() { res := make(chan interface{}, 10000) timeout := time.After(10 * time.Second) DoTickerWork(res, timeout) for v := range res { fmt.Println(v) }}
程式返回結果
$go run ticker.gostart 1 th workerstart 2 th workerstart 3 th worker123