timer in Go's runtime

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

我們總是使用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就是定時器逾時的時間
  • fvarg掛載的是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中執行的。梳理一下上面代碼的過程:

  1. 判斷堆中是否有Timer? 如果沒有就將Timersrescheduling設定為true的狀態,true就代表timerproc goroutine被掛起,需要重新調度。這個重新調度的時刻就是在添加一個Timer進來的時候,會ready這個goroutine。這裡掛起goroutine使用的是runtime·park()函數。
  2. 如果堆中有Timer存在,就取出堆頂的一個Timer,判斷是否逾時。逾時後,就刪除Timer,執行Timer中掛載的方法。這一步是迴圈檢查堆,直到堆中沒有Timer或者沒有逾時的Timer為止。
  3. 在堆中的Timer還沒逾時之前,這個goroutine將處於sleep狀態,也就是設定Timerssleeping為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中fvarg兩個欄位掛載的東西來完成的了。此處,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()實現總結起來就三大步:

  1. 建立一個Timer添加到Timers中
  2. 掛起當前goroutine
  3. Timer逾時ready當前goroutine

Go語言的定時器實現還是比較清晰的,沒有什麼繁瑣的邏輯。相比,其他地方(如:Nginx)的實現來說,這裡可能就是多了goroutine的調度邏輯。

看一個東西的實現,重要的是知道作者為何要這樣做。只有弄明白了why, how才有價值。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.