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!錯誤。

同時需要注意 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的時候都是通過 NewTimerAfterFunc 函數來獲取。
先來看一下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_createtimer_settimetimer_delete),而是通過四叉樹堆(heep)實現的(runtimeTimer 結構中的i字段,表示在堆中的索引)。通過構建一個最小堆,保證最快拿到到期了的定時器執行。定時器的執行,在專門的 goroutine 中進行的:go timerproc()。有興趣的同學,可以閱讀 runtime/time.go 的源碼。

其他方法

 

func After(d Duration) <-chan Time { return NewTimer(d).C }

 

根據源碼可以看到After直接是返回了Timerchannel,這種就可以做超時處理。
比如我們有這樣一個需求:我們寫了一個爬蟲,爬蟲在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類似於廢棄之前的定時器,重新啟動一個定時器,ResetTimer還未觸發時返回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就很簡單了,TimerTicker結構體的結構是一樣的,舉一反三,其實Ticker就是一個重復版本的Timer,它會重復的在時間d后向Ticker中寫數據

  • func NewTicker(d Duration) *Ticker // 新建一個Ticker
  • func (t *Ticker) Stop() // 停止Ticker
  • func Tick(d Duration) <-chan Time // Ticker.C 的封裝

TickerTimer 類似,區別是: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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM