Go語言的定時器實質是單向通道,time.Timer結構體類型中有一個time.Time類型的單向chan,源碼(src/time/time.go)如下
type Timer struct {
C <-chan Time
r runtimeTimer
初始化 Timer 方法為NewTimer
package main import ( "fmt" "time" ) func main() { t := time.NewTimer(time.Second * 2) defer t.Stop() for { <-t.C fmt.Println("timer running...") // 需要重置Reset 使 t 重新開始計時 t.Reset(time.Second * 2) } }
輸出
timer running…
timer running…
timer running…
timer running…
這里使用NewTimer定時器需要t.Reset重置計數時間才能接着執行。如果注釋 t.Reset(time.Second * 2)會導致通道堵塞,報fatal error: all goroutines are asleep - deadlock!錯誤。
同時需要注意 defer t.Stop()在這里並不會停止定時器。這是因為Stop會停止Timer,停止后,Timer不會再被發送,但是Stop不會關閉通道,防止讀取通道發生錯誤。
t := time.NewTimer(time.Second * 2) ch := make(chan bool) go func(t *time.Timer) { defer t.Stop() for { select { case <-t.C: fmt.Println("timer running....") // 需要重置Reset 使 t 重新開始計時 t.Reset(time.Second * 2) case stop := <-ch: if stop { fmt.Println("timer Stop") return } } } }(t) time.Sleep(10 * time.Second) ch <- true close(ch) time.Sleep(1 * time.Second)
定時器(NewTicker)
package main import ( "fmt" "time" ) func main() { t := time.NewTicker(time.Second*2) defer t.Stop() for { <- t.C fmt.Println("Ticker running...") } }
time.After
time.After()表示多長時間長的時候后返回一條time.Time類型的通道消息。但是在取出channel內容之前不阻塞,后續程序可以繼續執行。
先看源碼(src/time/sleep.go)
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
通過源碼我們發現它返回的是一個NewTimer(d).C,其底層是用NewTimer實現的,所以如果考慮到效率低,可以直接自己調用NewTimer。
package main import ( "fmt" "time" ) func main() { t := time.After(time.Second * 3) fmt.Printf("t type=%T\n", t) //阻塞3秒 fmt.Println("t=", <-t) }
基於time.After()特性可以配合select實現計時器
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int, 1) ch1 <- 1 for { select { case e1 := <-ch1: //如果ch1通道成功讀取數據,則執行該case處理語句 fmt.Printf("1th case is selected. e1=%v\n", e1) case <-time.After(time.Second*2): fmt.Println("Timed out") } } }
select語句阻塞等待最先返回數據的channel`,如ch1通道成功讀取數據,則先輸出1th case is selected. e1=1,之后每隔2s輸出 Timed out。
time.Timer
結構
首先我們看Timer
的結構定義:
type Timer struct { C <-chan Time r runtimeTimer }
其中有一個C
的只讀channel
,還有一個runtimeTimer
類型的結構體,再看一下這個結構的具體結構:
type runtimeTimer struct { tb uintptr i int when int64 period int64 f func(interface{}, uintptr) // NOTE: must not be closure arg interface{} seq uintptr }
在使用定時器Timer
的時候都是通過 NewTimer
或 AfterFunc
函數來獲取。
先來看一下NewTimer
的實現:
func NewTimer(d Duration) *Timer { c := make(chan Time, 1) t := &Timer{ C: c, r: runtimeTimer{ when: when(d), //表示達到時間段d時候調用f f: sendTime, // f表示一個函數調用,這里的sendTime表示d時間到達時向Timer.C發送當前的時間 arg: c, // arg表示在調用f的時候把參數arg傳遞給f,c就是用來接受sendTime發送時間的 }, } startTimer(&t.r) return t }
定時器的具體實現邏輯,都在
runtime
中的time.go
中,它的實現,沒有采用經典Unix
間隔定時器setitimer
系統調用,也沒有 采用POSIX
間隔式定時器(相關系統調用:timer_create
、timer_settime
和timer_delete
),而是通過四叉樹堆(heep
)實現的(runtimeTimer
結構中的i
字段,表示在堆中的索引)。通過構建一個最小堆,保證最快拿到到期了的定時器執行。定時器的執行,在專門的goroutine
中進行的:go timerproc()
。有興趣的同學,可以閱讀runtime/time.go
的源碼。
其他方法
func After(d Duration) <-chan Time { return NewTimer(d).C }
根據源碼可以看到After
直接是返回了Timer
的channel
,這種就可以做超時處理。
比如我們有這樣一個需求:我們寫了一個爬蟲,爬蟲在HTTP GET 一個網頁的時候可能因為網絡的原因就一只等待着,這時候就需要做超時處理,比如只請求五秒,五秒以后直接丟掉不請求這個網頁了,或者重新發起請求。
go Get("http://baidu.com/") func Get(url string) { response := make(chan string) response = http.Request(url) select { case html :=<- response: println(html) case <-time.After(time.Second * 5): println("超時處理") } }
可以從代碼中體現出來,如果五秒到了,網頁的請求還沒有下來就是執行超時處理,因為Timer
的內部會是幫你在你設置的時間長度后自動向Timer.C
中寫入當前時間。
其實也可以寫成這樣:
func Get(url string) { response := make(chan string) response = http.Request(url) timeOut := time.NewTimer(time.Second * 3) select { case html :=<- response: println(html) case <-timeOut.C: println("超時處理") } }
func (t *Timer) Reset(d Duration) bool
//強制的修改timer
中規定的時間,Reset
會先調用stopTimer
再調用startTimer
,類似於廢棄之前的定時器,重新啟動一個定時器,Reset
在Timer
還未觸發時返回true
;觸發了或Stop
了,返回false
。func (t *Timer) Stop() bool
// 如果定時器還未觸發,Stop
會將其移除,並返回true
;否則返回false
;后續再對該Timer
調用Stop
,直接返回false
。
比如我寫了了一個簡單的事例:每兩秒給你的女票發送一個"I Love You!"
// 其中協程之間的控制做的不太好,可以使用channel或者golang中的context來控制 package main import ( "time" "fmt" ) func main() { go Love() // 起一個協程去執行定時任務 stop := 0 for { fmt.Scan(&stop) if stop == 1{ break } } } func Love() { timer := time.NewTimer(2 * time.Second) // 新建一個Timer for { select { case <-timer.C: fmt.Println("I Love You!") timer.Reset(2 * time.Second) // 上一個when執行完畢重新設置 } } return }
func AfterFunc(d Duration, f func()) *Timer
// 在時間d后自動執行函數f
func main() { f := func(){fmt.Println("I Love You!")} time.AfterFunc(time.Second*2, f) time.Sleep(time.Second * 4) }
自動在2秒后打印 "I Love You!"
time.Ticker
如果學會了Timer
那么Ticker
就很簡單了,Timer
和Ticker
結構體的結構是一樣的,舉一反三,其實Ticker
就是一個重復版本的Timer
,它會重復的在時間d后向Ticker
中寫數據
func NewTicker(d Duration) *Ticker
// 新建一個Tickerfunc (t *Ticker) Stop()
// 停止Tickerfunc Tick(d Duration) <-chan Time
// Ticker.C 的封裝
Ticker
和 Timer
類似,區別是:Ticker
中的runtimeTimer
字段的 period
字段會賦值為 NewTicker(d Duration)
中的d
,表示每間隔d
納秒,定時器就會觸發一次。
除非程序終止前定時器一直需要觸發,否則,不需要時應該調用 Ticker.Stop
來釋放相關資源。
如果程序終止前需要定時器一直觸發,可以使用更簡單方便的 time.Tick
函數,因為 Ticker
實例隱藏起來了,因此,該函數啟動的定時器無法停止。
那么這樣我們就可以把發"I Love You!"
的例子寫得簡單一些。
func main() { //定義一個ticker ticker := time.NewTicker(time.Millisecond * 500) //Ticker觸發 go func() { for t := range ticker.C { fmt.Println(t) fmt.Println("I Love You!") } }() time.Sleep(time.Second * 18) //停止ticker ticker.Stop() }
定時器的實際應用
在實際開發中,定時器用的較多的會是 Timer
,如模擬超時,而需要類似 Tiker
的功能時,可以使用實現了 cron spec
的庫 cron。
首先time.Timer和 time.NewTicker屬於定時器,二者的區別在於
timer : 到固定時間后會執行一次,請注意是一次,而不是多次。但是可以通過reset來實現每隔固定時間段執行
ticker : 每隔固定時間都會觸發,多次執行. 具體請查看下面示例1
time.After : 用於實時超時控制,常見主要和select channel結合使用.查看代碼示例2
注意點:
沒有關閉定時器的執行。定時器未關閉!!!!大家會想到stop ,使用stop注意是在協程內還是攜程外,以及使用的場景業務
協程退出時需要關閉,避免資源l浪費,使用defer ticker.Stop()
package main import ( "fmt" "time" ) //定時器的stop func main() { // 協程內的定時器 stop 在協程結束時,關閉默認資源定時器,channel 具體根據業務來看 go func() { ticker := time.NewTicker(5 * time.Second) // 此處 可以簡化為defer ticker.Stop() defer func() { fmt.Println("stop") ticker.Stop() }() select { case <- ticker.C: fmt.Println("ticker..." ) } }() // 停止ticker stopChan := make(chan bool) ticker := time.NewTicker(5 * time.Second) go func(ticker *time.Ticker) { defer func() { ticker.Stop() fmt.Println("Ticker2 stop") }() for { select { case s := <-ticker.C: fmt.Println("Ticker2....",s) case stop := <-stopChan: if stop { fmt.Println("Stop") return } } } }(ticker) // 此處的stop 並不會結束上面協程,也不會打印出 Ticker2 stop 只能借助stopChan,讓協程結束時關閉ticker或者協程出現panic時執行defer //ticker.Stop() stopChan <- true close(stopChan) time.Sleep(time.Second * 10) fmt.Println("main end") }
timer正確的stop 問題
使用 Golang Timer 的正確方式
https://www.codercto.com/a/34856.html
一、標准 Timer 的問題
以下討論只針對由 NewTimer 創建的 Timer,因為這種 Timer 會使用 channel 來傳遞到期事件,而正確操作 channel 並非易事。
Timer.Stop
按照 Timer.Stop 文檔 的說法,每次調用 Stop 后需要判斷返回值,如果返回 false(表示 Stop 失敗,Timer 已經在 Stop 前到期)則需要排掉(drain)channel 中的事件:
if !t.Stop() { <-t.C }
但是如果之前程序已經從 channel 中接收過事件,那么上述 <-t.C
就會發生阻塞。可能的解決辦法是借助 select 進行 非阻塞 排放(draining):
if !t.Stop() { select { case <-t.C: // try to drain the channel default: } }
但是因為 channel 的發送和接收發生在不同的 goroutine,所以 存在競爭條件 (race condition),最終可能導致 channel 中的事件未被排掉。
以下就是一種有問題的場景,按時間先后順序發生:
select...case <-t.C
Timer.Reset
按照 Timer.Reset 文檔 的說法,要正確地 Reset Timer,首先需要正確地 Stop Timer。因此 Reset 的問題跟 Stop 基本相同。
二、使用 Timer 的正確方式
參考 Russ Cox 的回復( 這里 和 這里 ),目前 Timer 唯一合理的使用方式是:
- 程序始終在同一個 goroutine 中進行 Timer 的 Stop、Reset 和 receive/drain channel 操作
- 程序需要維護一個狀態變量,用於記錄它是否已經從 channel 中接收過事件,進而作為 Stop 中 draining 操作的判斷依據
如果每次使用 Timer 都要按照上述方式來處理,無疑是一件很費神的事。為此,我專門寫了一個 Go 庫 goodtimer 來解決標准 Timer 的問題。懶是一種美德 :-)
本文鏈接:https://www.codercto.com/a/34856.html
https://www.jianshu.com/p/372f714c2cf3
https://studygolang.com/articles/9289鏈接:https://www.jianshu.com/p/2b4686b8de4a
http://russellluo.com/2018/09/the-correct-way-to-use-timer-in-golang.html
參考;
https://blog.csdn.net/guyan0319/article/details/90450958