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
