golang 定時器相關的函數超硬核解析
一、前言
Golang 定時器包括:一次性定時器(Timer)和週期性定時器 (Ticker)。
編程中經常會通過 timer 和 ticker、AfterFunc。定時器 NewTicker 是設定每隔多長時間觸發的,是連續觸發,而計時器 NewTimer 是等待多長時間觸發的,只觸發一次,兩者是不同的。等待時間函數 AfterFunc 是在 After 基礎上加了一個回調函數,是等待時間到來後在另外一個 goroutine 協程裏調用。
1.1 定時器相關得函數
-
1、time.NewTicker():創建一個 Ticker 類型的定時器。
-
2、time.Ticker.C:返回一個定時的通道,每隔一段時間發送一個時間值。
-
3、time.Ticker.Stop():停止定時器。
-
4、time.NewTimer():創建一個 Timer 類型的定時器。
-
5、time.Timer.C:返回一個通道,定時器到期後發送一個時間值。
-
6、time.Timer.Reset():重新設置定時器到期時間。
-
7、time.Timer.Stop():停止定時器。
二、詳細說明
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中推送當前時間。
}
-
當 timer 過期後,當前時間將會被髮送到 C
-
timer 只能被 NewTimer 或者 AfterFunc 兩個函數創建
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 是一個帶 1 個容量的 chan,這樣做有什麼好處呢,原因是 chan 無緩衝發送數據就會阻塞,阻塞系統協程,這顯然是不行的。
-
回調函數設置爲 sendTime,執行參數爲 channel,sendTime 就是到點往 C 裏面發送當前時間的函數
//c interface{} 就是NewTimer 賦值的參數,就是channel
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now(): //寫不進去的話,C 已滿,走default 分支
default:
}
}
-
sendTime 是不阻塞的,在 Timer 實現裏面是不會被阻塞的,因爲只寫一次數據。但是在 Ticker 裏面就會存在阻塞,因爲容量爲 1,ticker 會按時間間隔週期性的寫數據到 C,這時候如果沒有寫進去,這次寫事件就會丟棄。那麼是怎麼做到呢?
case c.(chan Time) <- Now() 的時候,如果 C 裏面的數據沒人取走,那麼 C 已滿,case 這條分支發送數據到 C 就會執行失敗而走下面的 default。相當於本次調用沒有任何操作。 -
官方註釋說:如果 reader 讀 C 數據慢於第二次向 C 寫數據,那麼丟掉這次數據是理想的行爲。
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 的優化都十分重要
-
Go 1.9 版本之前,使用全局唯一的四叉堆維護
-
Go 1.10-1.13,全局使用 64 個四叉堆,每個處理器(P)對應一個四叉堆
-
Go 1.14 版本之後,每個處理器 P 直接管理一個四叉堆,通過網絡輪詢器觸發
2.3.2 四叉堆說明
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/nNnTAagRtWoRjplRTF-PXA