1. 程式人生 > 其它 >golang timer定時器

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的時候都是通過 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 問題

 

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