圖解goroutine調(diào)度

前言

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

模型

三個小伙子

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

G

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

M

M代表著真正的執(zhí)行計算資源警医。在綁定有效的P后亿胸,進入調(diào)度器循環(huán);而調(diào)度器循環(huán)的機制大致是從各種隊列预皇、P的本地隊列中獲取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對象隊列爬橡、鏈表治唤、一些cache和狀態(tài)。

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

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


image

看到這個圖你應(yīng)該理解了一半,下面我來說明一下柜裸。
我們知道缕陕,在操作系統(tǒng)眼里其實只有cpu和線程,它去控制著各個線程的調(diào)度粘室,切換榄檬,執(zhí)行等等卜范,而對于goroutine的實現(xiàn)其實非常類似衔统;
在golang層面,首先我們要知道海雪,最終在外面執(zhí)行的肯定是線程锦爵,但是我們內(nèi)部開出的那些goroutine到哪里去了呢?
golang提出GPM模型奥裸,在G的眼里险掀,只有P,P保存了需要執(zhí)行的那些goroutine湾宙;
而在整個go調(diào)度的層面樟氢,對外的是M冈绊,P會找到一個M,讓它去與外面的線程交互埠啃,從而去真正執(zhí)行程序死宣。
從這里我們可以發(fā)現(xiàn),其實goroutine的調(diào)度器整個就是一個小型的操作系統(tǒng)碴开,內(nèi)部去造出了類似的部件去完成goroutine的實現(xiàn)毅该,而因為是在內(nèi)部實現(xiàn),所以解決了操作系統(tǒng)層面所帶來的線程創(chuàng)建慢等問題潦牛。

但是眶掌,同時這樣的調(diào)度也會有問題,所以需要一些額外的措施巴碗!

調(diào)度中的問題

問題1 G不均

我們知道朴爬,現(xiàn)實情況有的goroutine運行的快,有的慢橡淆,那么勢必肯定會帶來的問題就是寝殴,忙的忙死,閑的閑死明垢,go肯定不允許摸魚的P存在蚣常,勢必要榨干所有勞動力。
如果你沒有任務(wù)痊银,那么抵蚊,我們看到模型中還有一個全局G隊列的存在,如本地隊列已滿溯革,會一次性轉(zhuǎn)移半數(shù)到全局隊列贞绳。其他閑的小伙子就會從全局隊列中拿;(順便說一下致稀,優(yōu)先級是先拿下一個需要執(zhí)行的冈闭,然后去本地隊列中拿,再去全局隊列中拿抖单,先把自己的做完再做別人的嘛)同時如果全局都沒有了萎攒,就會去搶別人的做。

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

萬一有個程序員啟動一個goroutine去執(zhí)行一個任務(wù)矛绘,然后這個任務(wù)一直睡覺(sleep)就是循環(huán)睡覺耍休,那咋辦嘛!你作為執(zhí)行人货矮,你總不能說羊精,讓它一直占用著一整個線程的資源,然后后面的goroutine都卡主了囚玫,那如果只有一個核心P喧锦,不就完蛋了读规?聰明的go才不會那么傻,它采用了搶占式調(diào)度來解決這個問題燃少。只要你這個任務(wù)執(zhí)行超過一定的時間(10ms)掖桦,那么這個任務(wù)就會被標(biāo)識為可搶占的,那么別的goroutine就可以搶先進來執(zhí)行供汛。只要下次這個goroutine進行函數(shù)調(diào)用枪汪,那么就會被強占,同時也會保護現(xiàn)場怔昨,然后重新放入P的本地隊列里面等待下次執(zhí)行雀久。
誰來做的呢?
sysmon趁舀,就是這個背后默默付出的人赖捌,它是一個后臺運行的監(jiān)控線程,它來監(jiān)控那些長時間運行的G任務(wù)然后設(shè)置可以強占的標(biāo)識符矮烹。(同時順便提一下越庇,它還會做的一些事情,例如奉狈,釋放閑置的span內(nèi)存卤唉,2分鐘的默認(rèn)gc等)

問題3 阻塞可怎么辦?

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

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

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

源碼一瞥

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

struct M
{
    G*    g0;        // 帶有調(diào)度棧的goroutine
    G*    gsignal;    // signal-handling G 處理信號的goroutine
    void    (*mstartfn)(void);
    G*    curg;        // M中當(dāng)前運行的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只是一個編號
    bool    blockingsyscall;
    bool    spinning;
    Note    park;
    M*    alllink;    // 這個域用于鏈接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)度時將它加一
    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è)計總的來說就是參考操作系統(tǒng)的設(shè)計蓖康,所有的目的很明確就是為了在整個運行過程中能充分利用已有的資源铐炫,盡可能在有限的資源里面多做事情,利用gpm的模型以及一些netpoller蒜焊、sysmon等幫助在阻塞的時候也能合理利用資源倒信,從而達到我們現(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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市泳梆,隨后出現(xiàn)的幾起案子鳖悠,更是在濱河造成了極大的恐慌,老刑警劉巖优妙,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乘综,死亡現(xiàn)場離奇詭異,居然都是意外死亡套硼,警方通過查閱死者的電腦和手機卡辰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人九妈,你說我怎么就攤上這事反砌。” “怎么了萌朱?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵宴树,是天一觀的道長。 經(jīng)常有香客問我晶疼,道長酒贬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任翠霍,我火速辦了婚禮锭吨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘壶运。我一直安慰自己耐齐,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布蒋情。 她就那樣靜靜地躺著埠况,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棵癣。 梳的紋絲不亂的頭發(fā)上辕翰,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音狈谊,去河邊找鬼喜命。 笑死,一個胖子當(dāng)著我的面吹牛河劝,可吹牛的內(nèi)容都是我干的壁榕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赎瞎,長吁一口氣:“原來是場噩夢啊……” “哼牌里!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起务甥,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤牡辽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后敞临,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體态辛,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年挺尿,在試婚紗的時候發(fā)現(xiàn)自己被綠了奏黑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炊邦。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖攀涵,靈堂內(nèi)的尸體忽然破棺而出铣耘,到底是詐尸還是另有隱情洽沟,我是刑警寧澤以故,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站裆操,受9級特大地震影響怒详,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜踪区,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一昆烁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缎岗,春花似錦静尼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至眷细,卻和暖如春拦盹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背溪椎。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工普舆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人校读。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓沼侣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親歉秫。 傳聞我的和親對象是個殘疾皇子蛾洛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容