圖解goroutine調(diào)度

前言

其實(shí)這個(gè)話題我早就想做了,奈何這個(gè)問題確實(shí)有點(diǎn)復(fù)雜,看了很多文章才有了一點(diǎn)點(diǎn)自己的理解。從golang一開始的使用我就已經(jīng)開始好奇了,這個(gè)goroutine到底是怎么實(shí)現(xiàn)的呢?怎么就能搞出一個(gè)和線程類似,但是性能又那么好的東西的呢?

模型

三個(gè)小伙子

在看整體結(jié)構(gòu)之前,我先來介紹三個(gè)小伙子,golang為了實(shí)現(xiàn)goroutine,定義了這樣三個(gè)小伙子,讓他們幫忙去實(shí)現(xiàn)。

G

表示goroutine,存儲(chǔ)了goroutine的執(zhí)行stack信息、goroutine狀態(tài)以及goroutine的任務(wù)函數(shù)等;另外G對象是可以重用的。

M

M代表著真正的執(zhí)行計(jì)算資源。在綁定有效的P后,進(jìn)入調(diào)度器循環(huán);而調(diào)度器循環(huán)的機(jī)制大致是從各種隊(duì)列、P的本地隊(duì)列中獲取G,切換到G的執(zhí)行棧上并執(zhí)行G的函數(shù),調(diào)用goexit做清理工作并回到M,如此反復(fù)。M并不保留G狀態(tài),這是G可以跨M調(diào)度的基礎(chǔ)。

P

表示邏輯processor,P的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的G的數(shù)量(前提:系統(tǒng)的物理cpu核數(shù)>=P的數(shù)量);P的最大作用還是其擁有的各種G對象隊(duì)列、鏈表、一些cache和狀態(tài)。

整體結(jié)構(gòu)模型

我們先來看下面的這張圖,從大體結(jié)構(gòu)上看,我們就能理解goroutine了


image

看到這個(gè)圖你應(yīng)該理解了一半,下面我來說明一下。
我們知道,在操作系統(tǒng)眼里其實(shí)只有cpu和線程,它去控制著各個(gè)線程的調(diào)度,切換,執(zhí)行等等,而對于goroutine的實(shí)現(xiàn)其實(shí)非常類似;
在golang層面,首先我們要知道,最終在外面執(zhí)行的肯定是線程,但是我們內(nèi)部開出的那些goroutine到哪里去了呢?
golang提出GPM模型,在G的眼里,只有P,P保存了需要執(zhí)行的那些goroutine;
而在整個(gè)go調(diào)度的層面,對外的是M,P會(huì)找到一個(gè)M,讓它去與外面的線程交互,從而去真正執(zhí)行程序。
從這里我們可以發(fā)現(xiàn),其實(shí)goroutine的調(diào)度器整個(gè)就是一個(gè)小型的操作系統(tǒng),內(nèi)部去造出了類似的部件去完成goroutine的實(shí)現(xiàn),而因?yàn)槭窃趦?nèi)部實(shí)現(xiàn),所以解決了操作系統(tǒng)層面所帶來的線程創(chuàng)建慢等問題。

但是,同時(shí)這樣的調(diào)度也會(huì)有問題,所以需要一些額外的措施!

調(diào)度中的問題

問題1 G不均

我們知道,現(xiàn)實(shí)情況有的goroutine運(yùn)行的快,有的慢,那么勢必肯定會(huì)帶來的問題就是,忙的忙死,閑的閑死,go肯定不允許摸魚的P存在,勢必要榨干所有勞動(dòng)力。
如果你沒有任務(wù),那么,我們看到模型中還有一個(gè)全局G隊(duì)列的存在,如本地隊(duì)列已滿,會(huì)一次性轉(zhuǎn)移半數(shù)到全局隊(duì)列。其他閑的小伙子就會(huì)從全局隊(duì)列中拿;(順便說一下,優(yōu)先級是先拿下一個(gè)需要執(zhí)行的,然后去本地隊(duì)列中拿,再去全局隊(duì)列中拿,先把自己的做完再做別人的嘛)同時(shí)如果全局都沒有了,就會(huì)去搶別人的做。

問題2 任務(wù)卡主了

萬一有個(gè)程序員啟動(dòng)一個(gè)goroutine去執(zhí)行一個(gè)任務(wù),然后這個(gè)任務(wù)一直睡覺(sleep)就是循環(huán)睡覺,那咋辦嘛!你作為執(zhí)行人,你總不能說,讓它一直占用著一整個(gè)線程的資源,然后后面的goroutine都卡主了,那如果只有一個(gè)核心P,不就完蛋了?聰明的go才不會(huì)那么傻,它采用了搶占式調(diào)度來解決這個(gè)問題。只要你這個(gè)任務(wù)執(zhí)行超過一定的時(shí)間(10ms),那么這個(gè)任務(wù)就會(huì)被標(biāo)識(shí)為可搶占的,那么別的goroutine就可以搶先進(jìn)來執(zhí)行。只要下次這個(gè)goroutine進(jìn)行函數(shù)調(diào)用,那么就會(huì)被強(qiáng)占,同時(shí)也會(huì)保護(hù)現(xiàn)場,然后重新放入P的本地隊(duì)列里面等待下次執(zhí)行。
誰來做的呢?
sysmon,就是這個(gè)背后默默付出的人,它是一個(gè)后臺(tái)運(yùn)行的監(jiān)控線程,它來監(jiān)控那些長時(shí)間運(yùn)行的G任務(wù)然后設(shè)置可以強(qiáng)占的標(biāo)識(shí)符。(同時(shí)順便提一下,它還會(huì)做的一些事情,例如,釋放閑置的span內(nèi)存,2分鐘的默認(rèn)gc等)

問題3 阻塞可怎么辦?

我們經(jīng)常使用goroutine還有一個(gè)場景就是網(wǎng)絡(luò)請求和IO操作,這種阻塞的情況下,我們的G和M又會(huì)怎么做呢?
這個(gè)時(shí)候有個(gè)叫做netpoller的東西出現(xiàn)了,當(dāng)每次有一個(gè)網(wǎng)絡(luò)請求阻塞的時(shí)候,如果按照原來的方法這個(gè)時(shí)候這個(gè)請求會(huì)阻塞線程,而有了netpoller這個(gè)東西,可以將請求阻塞到goroutine。
意思是說,當(dāng)阻塞出現(xiàn)的時(shí)候,當(dāng)前goroutine會(huì)被阻塞,等待阻塞notify,而放出M去做別的g,而當(dāng)阻塞恢復(fù)的時(shí)候,netpoller就會(huì)通知對應(yīng)的m可以做原來的g了。
同時(shí)還要順便提一句,當(dāng)P發(fā)現(xiàn)沒有任務(wù)的時(shí)候,除了會(huì)找本地和全局,也會(huì)去netpoll中找。

問題4 系統(tǒng)方法調(diào)用阻塞?

還有一個(gè)問題,我們自己想可能比較難想到,就是當(dāng)調(diào)用一些系統(tǒng)方法的時(shí)候,如果系統(tǒng)方法調(diào)用的時(shí)候發(fā)生阻塞就比較麻煩了。下面引用一段話:
當(dāng)G被阻塞在某個(gè)系統(tǒng)調(diào)用上時(shí),此時(shí)G會(huì)阻塞在_Gsyscall狀態(tài),M也處于block on syscall狀態(tài),此時(shí)的M可被搶占調(diào)度:執(zhí)行該G的M會(huì)與P解綁,而P則嘗試與其它idle的M綁定,繼續(xù)執(zhí)行其它G。如果沒有其它idle的M,但P的Local隊(duì)列中仍然有G需要執(zhí)行,則創(chuàng)建一個(gè)新的M;當(dāng)系統(tǒng)調(diào)用完成后,G會(huì)重新嘗試獲取一個(gè)idle的P進(jìn)入它的Local隊(duì)列恢復(fù)執(zhí)行,如果沒有idle的P,G會(huì)被標(biāo)記為runnable加入到Global隊(duì)列。

源碼一瞥

struct G
{
    uintptr    stackguard;    // 分段棧的可用空間下界
    uintptr    stackbase;    // 分段棧的棧基址
    Gobuf    sched;        //進(jìn)程切換時(shí),利用sched域來保存上下文
    uintptr    stack0;
    FuncVal*    fnstart;        // goroutine運(yùn)行的函數(shù)
    void*    param;        // 用于傳遞參數(shù),睡眠時(shí)其它goroutine設(shè)置param,喚醒時(shí)此goroutine可以獲取
    int16    status;        // 狀態(tài)Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
    int64    goid;        // goroutine的id號(hào)
    G*    schedlink;
    M*    m;        // for debuggers, but offset not hard-coded
    M*    lockedm;    // G被鎖定只能在這個(gè)m上運(yùn)行
    uintptr    gopc;    // 創(chuàng)建這個(gè)goroutine的go表達(dá)式的pc
    ...
};

struct M
{
    G*    g0;        // 帶有調(diào)度棧的goroutine
    G*    gsignal;    // signal-handling G 處理信號(hào)的goroutine
    void    (*mstartfn)(void);
    G*    curg;        // M中當(dāng)前運(yùn)行的goroutine
    P*    p;        // 關(guān)聯(lián)P以執(zhí)行Go代碼 (如果沒有執(zhí)行Go代碼則P為nil)
    P*    nextp;
    int32    id;
    int32    mallocing; //狀態(tài)
    int32    throwing;
    int32    gcing;
    int32    locks;
    int32    helpgc;        //不為0表示此m在做幫忙gc。helpgc等于n只是一個(gè)編號(hào)
    bool    blockingsyscall;
    bool    spinning;
    Note    park;
    M*    alllink;    // 這個(gè)域用于鏈接allm
    M*    schedlink;
    MCache    *mcache;
    G*    lockedg;
    M*    nextwaitm;    // next M waiting for lock
    GCStats    gcstats;
    ...
};

struct P
{
    Lock;
    uint32    status;  // Pidle或Prunning等
    P*    link;
    uint32    schedtick;   // 每次調(diào)度時(shí)將它加一
    M*    m;    // 鏈接到它關(guān)聯(lián)的M (nil if idle)
    MCache*    mcache;

    G*    runq[256];
    int32    runqhead;
    int32    runqtail;

    // Available G's (status == Gdead)
    G*    gfree;
    int32    gfreecnt;
    byte    pad[64];
};

struct Sched {
    Lock;

    uint64    goidgen;

    M*    midle;     // idle m's waiting for work
    int32    nmidle;     // number of idle m's waiting for work
    int32    nmidlelocked; // number of locked m's waiting for work
    int3    mcount;     // number of m's that have been created
    int32    maxmcount;    // maximum number of m's allowed (or die)

    P*    pidle;  // idle P's
    uint32    npidle;  //idle P的數(shù)量
    uint32    nmspinning;

    // Global runnable queue.
    G*    runqhead;
    G*    runqtail;
    int32    runqsize;

    // Global cache of dead G's.
    Lock    gflock;
    G*    gfree;

    int32    stopwait;
    Note    stopnote;
    uint32    sysmonwait;
    Note    sysmonnote;
    uint64    lastpoll;

    int32    profilehz;    // cpu profiling rate
}

總結(jié)

goroutine的設(shè)計(jì)總的來說就是參考操作系統(tǒng)的設(shè)計(jì),所有的目的很明確就是為了在整個(gè)運(yùn)行過程中能充分利用已有的資源,盡可能在有限的資源里面多做事情,利用gpm的模型以及一些netpoller、sysmon等幫助在阻塞的時(shí)候也能合理利用資源,從而達(dá)到我們現(xiàn)在高效的goroutine

參考資料:
https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.1.html
http://morsmachine.dk/go-scheduler
http://morsmachine.dk/netpoller
https://studygolang.com/articles/10116

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容