這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
我們總是使用sleep()類函數來讓線程暫停一段時間,在Go語言裡,也是使用Sleep()來暫停goroutine。那麼Go語言的sleep究竟是如何現實的呢?當然你翻看標準庫中的time包裡面的sleep.go源碼時,你可能會覺得看不明白,因為支援sleep功能的真正實現是在runtime裡面。不難想到sleep功能是根據定時器來實現的,因此接下來看看runtime中的timer究竟長什麼樣子。
timer的實現主要位於runtime/time.goc檔案中。
主要資料結構
struct Timers{ Lock; G *timerproc; bool sleeping; bool rescheduling; Note waitnote; Timer **t; int32 len; int32 cap;};struct Timer{ int32 i; // heap index // Timer wakes up at when, and then at when+period, ... (period > 0 only) // each time calling f(now, arg) in the timer goroutine, so f must be // a well-behaved function and not block. int64 when; int64 period; FuncVal *fv; Eface arg;};
這兩個結構是定義在runtime.h檔案中。
調用一次sleep其實就是產生一個Timer
,然後添加到Timers
中。可以看出來Timers就是維護所有Timer的一個集合。除了可以向Timers中添加Timer外,還要從Timers中刪除逾時的Timer。所以,Timers採用小頂堆來維護,小頂堆是常用來管理定時器的結構,有的地方也使用紅/黑樹狀結構。
Timers
- Timers結構中有一個
Lock
, 大概猜測一下就知道是用來保護添加/刪除
Timer的,實際上也是幹這件事的。
timerproc
指標維護的是一個goroutine,這個goroutine的主要功能就是檢查小頂堆中的Timer是否逾時。當然,逾時就是刪除Timer,並且執行Timer對應的動作。
t
顯然就是儲存所有Timer的堆了。
省略幾個欄位放到下文再介紹。
Timer
when
就是定時器逾時的時間
fv
和arg
掛載的是Timer逾時後需要執行的方法。
到此,Go語言的定時器大概模型就能想象出來了。其實,所有定時器的實現都大同小異,長得都差不多。
timerproc goroutine
上文提到timerproc維護的是一個goroutine,這個goroutine就做一件事情——不斷的迴圈檢查堆,刪除掉那些逾時的Timer,並執行Timer。下面精簡一下代碼,看個大概主幹就足夠明白了。
static voidtimerproc(void){ for(;;) { for(;;) { // 判斷Timer是否逾時 t = timers.t[0]; delta = t->when - now; if(delta > 0) break; // TODO: 刪除Timer, 代碼被刪除 // 這裡的f調用就是執行Timer了 f(now, arg); } // 這個過程是,堆中沒有任何Timer的時候,就把這個goroutine給掛起,不運行。 // 添加Timer的時候才會讓它ready。 if(delta < 0) { // No timers left - put goroutine to sleep. timers.rescheduling = true; runtime·park(runtime·unlock, &timers, "timer goroutine (idle)"); continue; } // 這裡乾的時候就讓這個goroutine也sleep, 等待最近的Timer逾時,再開始執行上面的迴圈檢查。當然,這裡的sleep不是用本文的定時器來實現的,而是futex鎖實現。 // At least one timer pending. Sleep until then. timers.sleeping = true; runtime·notetsleep(&timers.waitnote, delta); } }}
這裡一定要記住,timerproc
是在一個獨立的goroutine中執行的。梳理一下上面代碼的過程:
- 判斷堆中是否有Timer? 如果沒有就將
Timers
的rescheduling
設定為true的狀態,true就代表timerproc goroutine被掛起,需要重新調度。這個重新調度的時刻就是在添加一個Timer進來的時候,會ready這個goroutine。這裡掛起goroutine使用的是runtime·park()函數。
- 如果堆中有Timer存在,就取出堆頂的一個Timer,判斷是否逾時。逾時後,就刪除Timer,執行Timer中掛載的方法。這一步是迴圈檢查堆,直到堆中沒有Timer或者沒有逾時的Timer為止。
- 在堆中的Timer還沒逾時之前,這個goroutine將處於sleep狀態,也就是設定
Timers
的sleeping
為true狀態。這個地方是通過runtime·notesleep()函數來完成的,其實現是依賴futex鎖。這裡,goroutine將sleep多久呢?它將sleep到最近一個Timer逾時的時候,就開始執行。
維護Timers逾時的goroutine乾的所有事情也就這麼一點,這裡除了堆的維護外,就是goroutine的調度了。
添加一個定時器
另外一個重要的過程就是如何完成一個Timer的添加? 同樣精簡掉代碼,最好是對照完整的源碼看。
static voidaddtimer(Timer *t){ if(timers.len >= timers.cap) { // TODO 這裡是堆沒有剩餘的空間了,需要分配一個更大的堆來完成添加Timer。 } // 這裡添加Timer到堆中. t->i = timers.len++; timers.t[t->i] = t; siftup(t->i); // 這個地方比較重要,這是發生在添加的Timer直接位於堆頂的時候,堆頂位置就代表最近的一個逾時Timer. if(t->i == 0) { // siftup moved to top: new earliest deadline. if(timers.sleeping) { timers.sleeping = false; runtime·notewakeup(&timers.waitnote); } if(timers.rescheduling) { timers.rescheduling = false; runtime·ready(timers.timerproc); } }}
從代碼可以看到新添加的Timer如果是堆頂的話,會檢查Timers
的sleeping和rescheduling兩個狀態。上文已經提過了,這兩個狀態代表timeproc goroutine的狀態,如果處於sleeping,那就wakeup它; 如果是rescheduling就ready它。這麼做的原因就是通知那個wait的goroutine——”堆中有一個Timer了”或者”堆頂的Timer易主了”,你趕緊來檢查一下它是否逾時。
添加一個Timer的過程實在太簡單了,關鍵之處就是最後的Timers狀態檢查邏輯。
Sleep()的實現
上面的內容闡述了runtime的定時器是如何啟動並執行,那麼Go語言又是如何在定時器的基礎上實現Sleep()呢?
Go程式中調用time.Sleep()後將進入runtime,執行下面的代碼:
voidruntime·tsleep(int64 ns, int8 *reason){ Timer t; if(ns <= 0) return; t.when = runtime·nanotime() + ns; t.period = 0; t.fv = &readyv; t.arg.data = g; runtime·lock(&timers); addtimer(&t); runtime·park(runtime·unlock, &timers, reason);}
sleep原來就是建立一個Timer,添加到Timers中去,最後調用runtime·park()將當前調用Sleep()的goroutine給掛起就完事了。
關鍵是,goroutine被掛起後,如何在逾時後被喚醒繼續運行呢?這裡就是Timer中fv
和arg
兩個欄位掛載的東西來完成的了。此處,fv掛載了&readyv
,看一下readyv的定義:
static voidready(int64 now, Eface e){ USED(now); runtime·ready(e.data);}static FuncVal readyv = {(void(*)(void))ready};
readyv其實就是指向了ready函數,這個ready函數就是在Timer逾時的時候將會被執行,它將ready被掛起的goroutine。t.arg.data = g;
這行代碼就是在儲存當前goroutine了。
Sleep()實現總結起來就三大步:
- 建立一個Timer添加到Timers中
- 掛起當前goroutine
- Timer逾時ready當前goroutine
Go語言的定時器實現還是比較清晰的,沒有什麼繁瑣的邏輯。相比,其他地方(如:Nginx)的實現來說,這裡可能就是多了goroutine的調度邏輯。
看一個東西的實現,重要的是知道作者為何要這樣做。只有弄明白了why, how才有價值。