golang 定時器相關的函數超硬核解析

一、前言

Golang 定時器包括:一次性定時器(Timer)和週期性定時器 (Ticker)。

編程中經常會通過 timer 和 ticker、AfterFunc。定時器 NewTicker 是設定每隔多長時間觸發的,是連續觸發,而計時器 NewTimer 是等待多長時間觸發的,只觸發一次,兩者是不同的。等待時間函數 AfterFunc 是在 After 基礎上加了一個回調函數,是等待時間到來後在另外一個 goroutine 協程裏調用。

1.1 定時器相關得函數

二、詳細說明

2.1 Timer

2.1.1 說明

timer 創建有兩種方式,time.NewTimer(Duration) 和 time.After(Duration)。後者只是對前者的一個包裝。

timer 到固定時間後會執行一次,請注意是一次,而不是多次。但是可以通過 reset 來實現每隔固定時間段執行。使用 timer 定時器,超時後需要重置,才能繼續觸發。

2.1.2 Timer 案例

import (
  "fmt"
  "time"
)
func main() {
  myTimer := time.NewTimer(time.Second * 10) // 啓動定時器
  var i int = 0
  select {
  case <-myTimer.C:
    i++
    fmt.Println("count: ", i)
  }
}

2.1.3 Timer 數據結構

type Timer struct {
    C <-chan Time // 拋出來的channel,給上層系統使用,實現定時
    r runtimeTimer // 給系統管理使用的定時器,系統通過該字段確定定時器是否到時,如果到時,調用對應的函數向C中推送當前時間。
}
type runtimeTimer struct {
    pp       uintptr
    when     int64 //什麼時候觸發timer
    period   int64 //如果是週期性任務,執行週期性任務的時間間隔
    f        func(interface{}, uintptr) // NOTE: must not be closure//到時候執行的回調函數
    arg      interface{} //執行任務的參數
    seq      uintptr//回調函數的參數,該參數僅在 netpoll 的應用場景下使用。
    nextwhen int64//如果是週期性任務,下次執行任務時間
    status   uint32//timer 的狀態
}

那麼定時器是如何實現的呢?首先看一下定時器的構造:

//創建一個將會在Duration 時間後將那一刻的時間發生到C 的timer
func NewTimer(d Duration) *Timer {
   c := make(chan Time, 1)  //創建1個channel
   t := &Timer{ //創建一個timer
      C: c,
      r: runtimeTimer{
         when: when(d), //什麼時候執行
         f:    sendTime,  //到時候執行的回調函數
         arg:  c,//執行參數
      },
   }
   startTimer(&t.r) //開始timer
   return t
}
//c interface{} 就是NewTimer 賦值的參數,就是channel
func sendTime(c interface{}, seq uintptr) {
    select {
    case c.(chan Time) <- Now(): //寫不進去的話,C 已滿,走default 分支
    default:
    }
}

2.2 Ticker

2.2.1 說明

ticker 只要定義完成,從此刻開始計時,不需要任何其他的操作,每隔固定時間都會觸發。它會以一個間隔 (interval) 往通道發送當前時間,而通道的接收者可以以固定的時間間隔從通道中讀取時間。

2.2.2 案例說明

package main
import (
    "fmt"
    "time"
)
func main()  {
    t:=time.NewTicker(1*time.Second)
    defer t.Stop()
    for now:=range t.C{
        fmt.Println(now)
    }
}

週期性定時器到期了之後同樣是執行 sendTime 方法,這個上面已經描述過了。細心的你肯定注意到了,在 tickerDemo 中有一個 defer 去停止 ticker,爲什麼要這麼做呢?前面分析的時候講到,創建定時器就是把定時器的 runtimeTimer 放到由維護協程維護的堆中,一次性定時器到期後,會從堆中刪除,如果沒有到期則調用 Stop 方法實現刪除。但是,週期性定時器是不會執行刪除動作的,所以如果項目裏面持續創建多個週期性定時器並沒有 stop 的話,會導致堆越來越大,從而引起資源泄露。

經過代碼驗證:time.NewTicker 定時觸發執行任務,當下一次執行到來而當前任務還沒有執行結束時,會等待當前任務執行完畢後再執行下一次任務。

2.2.3 數據結構

type Ticker struct {
   C <-chan Time  //chan 定時到了以後,go 系統會忘裏面添加一個當前時間的數據
   r runtimeTimer 
}

創建一個 Ticker

func NewTicker(d Duration) *Ticker {
   if d <= 0 {
      panic(errors.New("non-positive interval for NewTicker"))
   }
  //這裏預留一個緩衝給timer 一樣,但是滿了以後沒人接收後面會丟掉事件
   c := make(chan Time, 1)
   t := &Ticker{
      C: c,
      r: runtimeTimer{
         when:   when(d),
         period: int64(d),
         f:      sendTime, //和ticker 的函數一樣
         arg:    c,
      },
   }
   startTimer(&t.r)
   return t
}

和 timer 創建方式一樣,只不過 period 爲 Duration,這樣底層在檢查時會根據這個字段判斷是不是週期性 timer,從而刪掉原來的 timer, 創建新的 timer

2.3 總結

2.3.1 特殊說明

上面的兩種計時器都會在底層創建一個 runtimeTimer,所以每一個版本中 runtimeTimer 的優化都十分重要

2.3.2 四叉堆說明

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/nNnTAagRtWoRjplRTF-PXA