golang timer定時器
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!錯誤。
定時器(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
// 新建一個Ticker -
func (t *Ticker) Stop()
// 停止Ticker -
func 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 問題
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