詳解Golang中NewTimer計(jì)時(shí)器的底層實(shí)現(xiàn)原理
1.簡介
本文將介紹 Go 語言中的NewTimer
,首先展示基于NewTimer
創(chuàng)建的定時(shí)器來實(shí)現(xiàn)超時(shí)控制。接著通過一系列問題的跟進(jìn),展示了NewTimer
的底層實(shí)現(xiàn)原理。
2.基本使用
我們首先通過一個(gè)簡單的例子,來展示是怎么基于NewTimer
實(shí)現(xiàn)超時(shí)控制的。
假設(shè)有一個(gè)需求,要求在 5 秒鐘內(nèi)完成某個(gè)任務(wù),否則就認(rèn)為任務(wù)失敗。這時(shí)我們就可以使用 NewTimer
來實(shí)現(xiàn)超時(shí)控制。具體的實(shí)現(xiàn)步驟如下:
- 首先,基于
NewTimer
創(chuàng)建一個(gè) Timer 對象,設(shè)定超時(shí)時(shí)間為 5 秒鐘。 - 然后,啟動(dòng)一個(gè) goroutine 來執(zhí)行任務(wù),并在任務(wù)完成后向通道發(fā)送一個(gè)完成信號(hào)。
- 最后,使用 select 語句等待 Timer 的到期事件或任務(wù)完成信號(hào),如果 Timer 先到期,就認(rèn)為任務(wù)超時(shí)了,否則就認(rèn)為任務(wù)完成了。
下面是一個(gè)簡單的實(shí)現(xiàn)代碼展示:
package main import ( "fmt" "time" ) func main() { // 創(chuàng)建一個(gè)定時(shí)器,超時(shí)時(shí)間為 5 秒鐘 timer := time.NewTimer(5 * time.Second) // 啟動(dòng)一個(gè) goroutine 執(zhí)行任務(wù),并在任務(wù)完成后向通道發(fā)送一個(gè)完成信號(hào) done := make(chan bool, 1) go func() { // 模擬任務(wù)執(zhí)行耗時(shí) time.Sleep(2 * time.Second) done <- true }() // 等待任務(wù)完成或者超時(shí) select { case <-done: // 任務(wù)完成,輸出完成信息 fmt.Println("Task finished.") case <-timer.C: // 超時(shí),輸出超時(shí)信息 fmt.Println("Task timed out.") } }
在上述代碼中,我們首先使用NewTimer
創(chuàng)建了一個(gè)time.Timer
對象,超時(shí)時(shí)間為 5 秒鐘。然后啟動(dòng)一個(gè) goroutine 來執(zhí)行任務(wù),并在任務(wù)完成后向通道 done
發(fā)送一個(gè)完成信號(hào)。最后使用 select
語句等待任務(wù)完成信號(hào)或者Timer
的到期事件,如果Timer
先到期,就認(rèn)為任務(wù)超時(shí)了,否則就認(rèn)為任務(wù)完成了。 在運(yùn)行上述代碼時(shí),我們可以看到,在任務(wù)完成前 5 秒鐘內(nèi),程序輸出如下信息:
Task finished.
如果將任務(wù)完成時(shí)間改為超過 5 秒鐘,程序?qū)?huì)在 5 秒鐘后超時(shí),輸出如下信息:
Task timed out.
通過這個(gè)簡單的例子,我們可以看到,如果任務(wù)在指定超時(shí)時(shí)間內(nèi)完成,此時(shí)會(huì)執(zhí)行正常的業(yè)務(wù)邏輯;如果任務(wù)未在指定的超時(shí)時(shí)間內(nèi)完成,此時(shí)將走執(zhí)行超時(shí)邏輯。
通過上述程序演示,我們展示了如何使用NewTimer
創(chuàng)建的 time.Timer
實(shí)現(xiàn)超時(shí)控制的基本方法。
3.實(shí)現(xiàn)原理
3.1 內(nèi)容分析
回顧上面的示例代碼,我們實(shí)現(xiàn)超時(shí)控制的主要機(jī)制,是通過select
語句同時(shí)監(jiān)聽兩個(gè)channel
,一個(gè)是任務(wù)執(zhí)行狀態(tài)的channel
,一個(gè)是定時(shí)器的channel
。
當(dāng)任務(wù)執(zhí)行完成時(shí),便通過channel
對主協(xié)程進(jìn)行通知。當(dāng)定時(shí)器到達(dá)我們指定的時(shí)間,也就是超時(shí)時(shí)間,此時(shí)也通過定時(shí)器的channel
進(jìn)行通知。
同時(shí)監(jiān)聽這兩個(gè)channel
,如果任務(wù)先執(zhí)行完成,此時(shí)將會(huì)走select
語句中正常業(yè)務(wù)邏輯case
的代碼,如果是在到達(dá)預(yù)定時(shí)間,任務(wù)仍沒有完成,此時(shí)通過定時(shí)器channel
進(jìn)行通知,從而走超時(shí)業(yè)務(wù)邏輯case
的代碼,從而實(shí)現(xiàn)超時(shí)控制。
因此,這里主要的問題是,是如何在到達(dá)超時(shí)時(shí)間時(shí),準(zhǔn)時(shí)往定時(shí)器中的C
對應(yīng)的channel
發(fā)送送數(shù)據(jù),從而來告知其他協(xié)程,已經(jīng)到達(dá)超時(shí)時(shí)間了呢,這個(gè)是如何做到的呢?
3.2 基本思路
下面先來看看NewTimer
方法返回Timer
結(jié)構(gòu)體的內(nèi)容,定義如下:
type Timer struct { C <-chan Time r runtimeTimer }
可以看到,Timer
結(jié)構(gòu)體中C是一個(gè)chan Time
類型的變量。在前面的代碼的例子中,select
語句是監(jiān)聽Timer
結(jié)構(gòu)體中的C
變量,從中來讀取數(shù)據(jù)的。
那么,當(dāng)?shù)竭_(dá)超時(shí)時(shí)間時(shí),Timer
中C
對應(yīng)的channel
將會(huì)有數(shù)據(jù)到達(dá),那么肯定有其他地方,在到達(dá)超時(shí)時(shí)間時(shí),會(huì)往Timer
中的C
發(fā)送數(shù)據(jù)。
那么現(xiàn)在的主要問題,是怎么做到當(dāng)?shù)竭_(dá)指定時(shí)間時(shí),往Timer
中的C
發(fā)送數(shù)據(jù)呢?
其實(shí),在go
語言中,存在這樣一個(gè)運(yùn)行時(shí)函數(shù)startTimer
,定義如下:
func startTimer(*runtimeTimer)
它的作用是啟動(dòng)一個(gè)定時(shí)器,當(dāng)定時(shí)器到期時(shí),會(huì)執(zhí)行相應(yīng)的回調(diào)函數(shù)并傳遞回調(diào)參數(shù)。在 startTimer
函數(shù)內(nèi)部,會(huì)使用系統(tǒng)調(diào)用來啟動(dòng)一個(gè)底層的操作系統(tǒng)定時(shí)器,等到定時(shí)器超時(shí)時(shí),底層系統(tǒng)會(huì)自動(dòng)觸發(fā)一個(gè)信號(hào)(例如 Unix 平臺(tái)上的 SIGALRM 信號(hào)),然后該信號(hào)將由 Go 運(yùn)行時(shí)內(nèi)部的信號(hào)處理函數(shù)捕獲,并最終調(diào)用相關(guān)的回調(diào)函數(shù)。
那么,這里我們似乎可以使用startTimer
來實(shí)現(xiàn),當(dāng)?shù)竭_(dá)指定時(shí)間時(shí),往channel
發(fā)送數(shù)據(jù),從而達(dá)到通知其他協(xié)程的效果。
3.3 實(shí)現(xiàn)步驟
首先,我們已經(jīng)知道,startTimer
能夠啟動(dòng)一個(gè)定時(shí)器,當(dāng)定時(shí)器到期時(shí),會(huì)執(zhí)行相應(yīng)的回調(diào)函數(shù)并傳遞回調(diào)參數(shù)。而定時(shí)器的到期時(shí)間、回調(diào)函數(shù)以及回調(diào)函數(shù)的參數(shù),則是通過runtimeTimer
結(jié)構(gòu)體傳遞過去的。
下面我們只需要runtimeTimer
字段的含義,然后根據(jù)其含義,正確設(shè)置runtimeTimer
結(jié)構(gòu)體字段,調(diào)用startTimer
方法啟動(dòng)一個(gè)定時(shí)器,就能夠?qū)崿F(xiàn)在指定時(shí)間時(shí),執(zhí)行某段邏輯。下面我們來看看runtimeTime
的定義:
type runtimeTimer struct { pp uintptr when int64 period int64 f func(any, uintptr) // NOTE: must not be closure arg any seq uintptr nextwhen int64 status uint32 }
下面對runtimeTimer
中的字段進(jìn)行說明:
when int64
:表示定時(shí)器應(yīng)該在何時(shí)觸發(fā)。該值的單位是納秒period int64
:表示定時(shí)器的重復(fù)周期。如果定時(shí)器不需要重復(fù)觸發(fā),則該值為 0f func(any, uintptr)
:指向定時(shí)器到期后需要執(zhí)行的函數(shù)。arg any
:表示傳遞給定時(shí)器到期后執(zhí)行的函數(shù)的參數(shù),將傳遞給f
的第一個(gè)參數(shù)nextwhen int64
:如果定時(shí)器是重復(fù)觸發(fā)的,則nextwhen
表示下一次觸發(fā)的時(shí)間。seq uintptr
:用于防止定時(shí)器被錯(cuò)誤的重置。當(dāng)定時(shí)器被重置時(shí),seq
會(huì)被更新。
基于對上面字段含義的理解,此時(shí)我們定義一個(gè)runtimeTimer
結(jié)構(gòu)體,然后調(diào)用startTimer
,從而來實(shí)現(xiàn)能夠在指定的某個(gè)時(shí)間點(diǎn),往某個(gè)channel
發(fā)送數(shù)據(jù)。具體實(shí)現(xiàn)如下:
// 定義一個(gè)channel,用于發(fā)送數(shù)據(jù) c := make(chan Time, 1) r := runtimeTimer{ // 指定超時(shí)時(shí)間戳 when: when(d), // 指定回調(diào)函數(shù) f: func sendTime(c any, seq uintptr) { select { case c.(chan Time) <- Now(): default: } }, // 傳遞給回調(diào)函數(shù)的參數(shù) arg: c, }, } // 調(diào)用startTimer啟動(dòng)一個(gè)定時(shí)器 startTimer(&t.r)
首先會(huì)創(chuàng)建一個(gè)帶有緩沖的通道 c
。
接著初始化runtimeTimer
結(jié)構(gòu)體的值,設(shè)定好超時(shí)時(shí)間,回調(diào)函數(shù)以及參數(shù)。超時(shí)時(shí)間使用的是when
函數(shù)來獲取計(jì)數(shù)器的結(jié)束時(shí)間。when
函數(shù)會(huì)根據(jù)給定的時(shí)間間隔 d,返回一個(gè)絕對時(shí)間點(diǎn),即計(jì)時(shí)器結(jié)束時(shí)間。
f
字段指定的回調(diào)函數(shù),則是將當(dāng)前時(shí)間 Now()
發(fā)送到通道 c
中。當(dāng)?shù)竭_(dá)指定超時(shí)時(shí)間時(shí),其將會(huì)調(diào)用回調(diào)函數(shù)f
,同時(shí)將runtimeTimer
結(jié)構(gòu)體中arg
字段的值,作為參數(shù)傳遞到回調(diào)函數(shù)當(dāng)中。
然后調(diào)用startTimer
啟動(dòng)一個(gè)定時(shí)器。當(dāng)?shù)竭_(dá)超時(shí)時(shí)間,將會(huì)調(diào)用回調(diào)函數(shù),回調(diào)函數(shù)其會(huì)往一開始定義的channel
發(fā)送數(shù)據(jù)。
至此,我們實(shí)現(xiàn)了最開始提到的,當(dāng)?shù)竭_(dá)指定時(shí)間時(shí),往Timer
中的C
發(fā)送數(shù)據(jù)這個(gè)任務(wù)。
3.4 NewTimer的實(shí)現(xiàn)
回到我們的題目,NewTimer計(jì)時(shí)器的底層實(shí)現(xiàn)原理是什么?事實(shí)上,NewTimer
創(chuàng)建的定時(shí)器,也確實(shí)是基于startTimer
來實(shí)現(xiàn)的,下面我們來看看其實(shí)現(xiàn):
func NewTimer(d Duration) *Timer { c := make(chan Time, 1) t := &Timer{ C: c, r: runtimeTimer{ when: when(d), f: sendTime, arg: c, }, } startTimer(&t.r) return t }
首先會(huì)創(chuàng)建一個(gè)帶有緩沖的通道 c
。然后創(chuàng)建一個(gè) Timer
對象 t
,將 c
通道賦值給 t
的 C
屬性。channel
之后將作為回調(diào)函數(shù)的參數(shù),同時(shí)也會(huì)作為Timer
對象中C
屬性的值。這樣子回調(diào)函數(shù)和Timer
結(jié)構(gòu)體中C
變量與回調(diào)函數(shù)的channel
事實(shí)上是共用一個(gè)channel
的。
而runtimeTimer
結(jié)構(gòu)體中的回調(diào)函數(shù)sendTime
的實(shí)現(xiàn)與之前講述的并無差異,都是將當(dāng)前時(shí)間 Now()
發(fā)送到通道中,這里將不再贅述。
而Timer
結(jié)構(gòu)體中C
變量與回調(diào)函數(shù)的channel
事實(shí)上是共用一個(gè)channel
的,當(dāng)?shù)竭_(dá)超時(shí)時(shí)間,則會(huì)執(zhí)行回調(diào)函數(shù),往channel
發(fā)送數(shù)據(jù)。而通過select
語句對Timer
中的channel
進(jìn)行監(jiān)聽的協(xié)程,此時(shí)也正常接收到通知了。
4.總結(jié)
在這篇文章中,我們首先展示了使用NewTimer
創(chuàng)建一個(gè)定時(shí)器,然后基于此實(shí)現(xiàn)超時(shí)控制。我們發(fā)現(xiàn),比較重要的一個(gè)機(jī)制是,timer
對象是在到達(dá)超時(shí)時(shí)間時(shí),timer
對象中C
對應(yīng)的channel
將會(huì)接收到數(shù)據(jù)。
timer
對象在達(dá)到超時(shí)時(shí)間時(shí),C
屬性對應(yīng)的channel
便能夠接收到數(shù)據(jù),這個(gè)是怎么做到的呢? 我們基于此進(jìn)行了分析,事實(shí)上,首先是基于startTimer
創(chuàng)建定時(shí)器,同時(shí)runtimeTimer
結(jié)構(gòu)體指定了回調(diào)函數(shù),從而實(shí)現(xiàn)達(dá)到超時(shí)時(shí)間時(shí),回調(diào)函數(shù)往C
屬性對應(yīng)的channel
發(fā)送數(shù)據(jù)。然后就能夠基于該特性,能夠?qū)崿F(xiàn)超時(shí)控制等機(jī)制。
到此這篇關(guān)于詳解Golang中NewTimer計(jì)時(shí)器的底層實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Golang NewTimer計(jì)時(shí)器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言中println和fmt.Println區(qū)別
本文主要介紹了Go語言中println和fmt.Println區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07詳解Golang time包中的time.Duration類型
在日常開發(fā)過程中,會(huì)頻繁遇到對時(shí)間進(jìn)行操作的場景,使用 Golang 中的 time 包可以很方便地實(shí)現(xiàn)對時(shí)間的相關(guān)操作,本文講解一下 time 包中的 time.Duration 類型,需要的朋友可以參考下2023-07-07Go語言:打造優(yōu)雅數(shù)據(jù)庫單元測試的實(shí)戰(zhàn)指南
Go語言數(shù)據(jù)庫單元測試入門:聚焦高效、可靠的數(shù)據(jù)庫代碼驗(yàn)證!想要確保您的Go應(yīng)用數(shù)據(jù)層堅(jiān)如磐石嗎?本指南將手把手教您如何利用Go進(jìn)行數(shù)據(jù)庫單元測試,輕松揪出隱藏的bug,打造無懈可擊的數(shù)據(jù)處理邏輯,一起來探索吧!2024-01-01