【深度知識(shí)】Golang協(xié)程調(diào)度:協(xié)程狀態(tài)

狀態(tài)總覽

在講解操作系統(tǒng)進(jìn)程調(diào)度的部分時(shí),幾乎所有的書籍都會(huì)先列出一張進(jìn)程的狀態(tài)遷移圖,通過狀態(tài)圖,能很清晰的把進(jìn)程調(diào)度的每個(gè)環(huán)節(jié)串聯(lián)起來,方便理解。

Go運(yùn)行時(shí)的調(diào)度器其實(shí)可以看成OS調(diào)度器的某種簡(jiǎn)化版 本,一個(gè)goroutine在其生命周期之中,同樣包含了各種狀態(tài)的變換。弄清了這些狀態(tài)及狀態(tài)間切換的原理,對(duì)搞清整個(gè)Go調(diào)度器會(huì)非常有幫助。

以下是我總結(jié)的一張goroutine的狀態(tài)遷移圖,圓形框表示狀態(tài),箭頭及文字信息表示切換的方向和條件:

狀態(tài)詳述

下面來簡(jiǎn)單分析一下, 其中狀態(tài) Gidle 在Go調(diào)度器代碼中并沒有被真正被使用到,所以直接忽略。 事實(shí)上,一旦runtime新建了一個(gè)goroutine結(jié)構(gòu),就會(huì)將其狀態(tài)置為Grunnable并加入到任務(wù)隊(duì)列中,因此我們以該狀態(tài)作為起點(diǎn)進(jìn)行介紹。

Grunnable

Golang中,一個(gè)協(xié)程在以下幾種情況下會(huì)被設(shè)置為 Grunnable狀態(tài):

創(chuàng)建

Go 語言中,包括用戶入口函數(shù)main·main的執(zhí)行g(shù)oroutine在內(nèi)的所有任務(wù),都是通過runtime·newproc -> runtime·newproc1 這兩個(gè)函數(shù)創(chuàng)建的,前者其實(shí)就是對(duì)后者的一層封裝,提供可變參數(shù)支持,Go語言的go關(guān)鍵字最終會(huì)被編譯器映射為對(duì)runtime·newproc的調(diào)用。當(dāng)runtime·newproc1完成了資源的分配及初始化后,新任務(wù)的狀態(tài)會(huì)被置為Grunnable,然后被添加到當(dāng)前 P 的私有任務(wù)隊(duì)列中,等待調(diào)度執(zhí)行。相關(guān)初始化代碼如下:

G* runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerpc) 
{
    G *newg;
    P *p;
    int32 siz;
    ......
    // 獲取當(dāng)前g所在的p,從p中創(chuàng)建一個(gè)新g(newg)
    p = g->m->p;
    if((newg = gfget(p)) == nil) {
        ......    
    }
    ......
    // 設(shè)置Goroutine狀態(tài)為Grunnable
    runtime·casgstatus(newg, Gdead,             
        Grunnable);
    .....
    // 新創(chuàng)建的g添加到run隊(duì)列中
    runqput(p, newg);
    ......
}

阻塞任務(wù)喚醒

當(dāng)某個(gè)阻塞任務(wù)(狀態(tài)為Gwaiting)的等待條件滿足而被喚醒時(shí)—如一個(gè)任務(wù)G#1向某個(gè)channel寫入數(shù)據(jù)將喚醒之前等待讀取該channel數(shù)據(jù)的任務(wù)G#2——G#1通過調(diào)用runtime·ready將G#2狀態(tài)重新置為Grunnable并添加到任務(wù)隊(duì)列中。關(guān)于任務(wù)阻塞,稍后還很詳細(xì)介紹。

// Mark gp ready to run.
void
runtime·ready(G *gp)
{
    uint32 status;
    status = runtime·readgstatus(gp);
    // Mark runnable.
    g->m->locks++;  
    if((status&~Gscan) != Gwaiting){
        dumpgstatus(gp);
        runtime·throw("bad g->status in ready");
    }
    // 設(shè)置被喚醒的g狀態(tài)從Gwaiting轉(zhuǎn)變至Grunnable
    runtime·casgstatus(gp, Gwaiting, Grunnable);
    // 添加到運(yùn)行隊(duì)列中
    runqput(g->m->p, gp);
    if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0) 
    // 看起來這是個(gè)比較重要的函數(shù),但還不是很理解
    wakep();
    g->m->locks--;
    if(g->m->locks == 0 && g->preempt) 
        g->stackguard0 = StackPreempt;
}

其他

另外的路徑是從Grunning和Gsyscall狀態(tài)變換到Grunnable,我們也都合并到后面介紹。 總之,處于Grunnable的任務(wù)一定在某個(gè)任務(wù)隊(duì)列中,隨時(shí)等待被調(diào)度執(zhí)行。

Grunning

所有狀態(tài)為Grunnable的任務(wù)都可能通過findrunnable函數(shù)被調(diào)度器(P&M)獲取,進(jìn)而通過execute函數(shù)將其狀態(tài)切換到Grunning, 最后調(diào)用runtime·gogo加載其上下文并執(zhí)行。

// One round of scheduler: find a runnable goroutine and execute it. Never returns.
static void
schedule(void)
{
    G *gp;
    uint32 tick;

    if(g->m->locks)
        runtime·throw("schedule: holding locks");

    if(g->m->lockedg) {
        stoplockedm();
        execute(g->m->lockedg);  // Never returns.
    }

top:
    if(runtime·sched.gcwaiting) {
        gcstopm();
        goto top;
    }

    gp = nil;
    // 挑一個(gè)可運(yùn)行的g,并執(zhí)行
    ......
    if(gp == nil) {
        gp = findrunnable();  // blocks until work is available
        resetspinning();
    }
    ......
    execute(gp);
}

// Schedules gp to run on the current M.
// Never returns.
static void
execute(G *gp)
{
    int32 hz;
    // 狀態(tài)從Grunnable轉(zhuǎn)變?yōu)镚running
    runtime·casgstatus(gp, Grunnable, Grunning);
    gp->waitsince = 0;
    gp->preempt = false;
    gp->stackguard0 = gp->stack.lo + StackGuard;
    g->m->p->schedtick++;
    g->m->curg = gp;
    gp->m = g->m;

    // Check whether the profiler needs to be turned on or off.
    hz = runtime·sched.profilehz;
    if(g->m->profilehz != hz)
        runtime·resetcpuprofiler(hz);
    // 真正執(zhí)行g(shù)
    runtime·gogo(&gp->sched);
}

前面講過Go本質(zhì)采用一種協(xié)作式調(diào)度方案,一個(gè)正在運(yùn)行的任務(wù),需要通過調(diào)用yield的方式顯式讓出處理器;在Go1.2之后,運(yùn)行時(shí)也開始支持一定程度的任務(wù)搶占——當(dāng)系統(tǒng)線程sysmon發(fā)現(xiàn)某個(gè)任務(wù)執(zhí)行時(shí)間過長(zhǎng)或者runtime判斷需要進(jìn)行垃圾收集時(shí),會(huì)將任務(wù)置為”可被搶占“的,當(dāng)該任務(wù)下一次函數(shù)調(diào)用時(shí), 就會(huì)讓出處理器并重新切會(huì)到Grunnable狀態(tài)。關(guān)于Go1.2中搶占機(jī)制的實(shí)現(xiàn)細(xì)節(jié),后面又機(jī)會(huì)再做介紹。

Gsyscall

Go運(yùn)行時(shí)為了保證高的并發(fā)性能,當(dāng)會(huì)在任務(wù)執(zhí)行OS系統(tǒng)調(diào)用前,先調(diào)用runtime·entersyscall函數(shù)將自己的狀態(tài)置為Gsyscall——如果系統(tǒng)調(diào)用是阻塞式的或者執(zhí)行過久,則將當(dāng)前M與P分離——當(dāng)系統(tǒng)調(diào)用返回后,執(zhí)行線程調(diào)用runtime·exitsyscall嘗試重新獲取P,如果成功且當(dāng)前任務(wù)沒有被搶占,則將狀態(tài)切回Grunning并繼續(xù)執(zhí)行;否則將狀態(tài)置為Grunnable,等待再次被調(diào)度執(zhí)行。

// Puts the current goroutine into a waiting state and calls unlockf.
// If unlockf returns false, the goroutine is resumed.
void
runtime·park(bool(*unlockf)(G*, void*), void *lock, String reason)
{
    void (*fn)(G*);

    g->m->waitlock = lock;
    g->m->waitunlockf = unlockf;
    g->waitreason = reason;
    fn = runtime·park_m;
    runtime·mcall(&fn);
}
// runtime·park continuation on g0.
void
runtime·park_m(G *gp)
{
    bool ok;
    // 設(shè)置當(dāng)前狀態(tài)從Grunning-->Gwaiting
    runtime·casgstatus(gp, Grunning, Gwaiting);
    // 當(dāng)前g放棄m
    dropg();

    if(g->m->waitunlockf) {
        ok = g->m->waitunlockf(gp, g->m->waitlock);
        g->m->waitunlockf = nil;
        g->m->waitlock = nil;
        if(!ok) {
            runtime·casgstatus(gp, Gwaiting, Grunnable);
            execute(gp);  // Schedule it back, never returns.
        }
    }

    schedule();
}

Gwaiting

當(dāng)一個(gè)任務(wù)需要的資源或運(yùn)行條件不能被滿足時(shí),需要調(diào)用runtime·park函數(shù)進(jìn)入該狀態(tài),之后除非等待條件滿足,否則任務(wù)將一直處于等待狀態(tài)不能執(zhí)行。除了之前舉過的channel的例子外,Go語言的定時(shí)器、網(wǎng)絡(luò)IO操作都可能引起任務(wù)的阻塞。

// runtime·park continuation on g0.
void
runtime·park_m(G *gp)
{
    bool ok;

    runtime·casgstatus(gp, Grunning, Gwaiting);
    dropg();

    if(g->m->waitunlockf) {
        ok = g->m->waitunlockf(gp, g->m->waitlock);
        g->m->waitunlockf = nil;
        g->m->waitlock = nil;
        if(!ok) {
            runtime·casgstatus(gp, Gwaiting, Grunnable);
            execute(gp);  // Schedule it back, never returns.
        }
    }

    schedule();
}

runtime·park函數(shù)包含3個(gè)參數(shù),第一個(gè)是解鎖函數(shù)指針,第二個(gè)是一個(gè)Lock指針,最后是一個(gè)字符串用以描述阻塞的原因。 很明顯,前兩個(gè)參數(shù)是配對(duì)的結(jié)構(gòu)——由于任務(wù)阻塞前可能獲得了某些Lock,這些Lock必須在任務(wù)狀態(tài)保存完成后才能釋放,以避免數(shù)據(jù)競(jìng)爭(zhēng)。我們知道channel必須通過Lock確保互斥訪問,一個(gè)阻塞的任務(wù)G#1需要將自己放到channel的等待隊(duì)列中,如果在完成上下文保存前就釋放了Lock,則可能導(dǎo)致G#2將未知狀態(tài)的G#1置為Grunnable,因此釋放Lock必須在runtime·park內(nèi)完成。 由于阻塞時(shí)任務(wù)持有的Lock類型不盡相同——如Select操作的鎖實(shí)際上是一組Lock的集合——因此需要特別指出Unlock的具體方式。 最后一個(gè)參數(shù)主要是在gdb調(diào)試的時(shí)候方便發(fā)現(xiàn)任務(wù)阻塞的原因。 順便說一下,當(dāng)所有的任務(wù)都處于Gwaiting狀態(tài)時(shí),也就表示當(dāng)前程序進(jìn)入了死鎖態(tài),不可能繼續(xù)執(zhí)行了,那么runtime會(huì)檢測(cè)到這種情況,并輸出所有Gwaiting任務(wù)的backtrace信息。

Gdead

最后,當(dāng)一個(gè)任務(wù)執(zhí)行結(jié)束后,會(huì)調(diào)用runtime·goexit結(jié)束自己的生命——將狀態(tài)置為Gdead,并將結(jié)構(gòu)體鏈到一個(gè)屬于當(dāng)前P的空閑G鏈表中,以備后續(xù)使用。

Go語言的并發(fā)模型基本上遵照了CSP模型,goroutine間完全靠channel通信,沒有像Unix進(jìn)程的wait或waitpid的等待機(jī)制,也沒有類似“POSIX Thread”中的pthread_join的匯合機(jī)制,更沒有像kill或signal這類的中斷機(jī)制。每個(gè)goroutine結(jié)束后就自行退出銷毀,不留一絲痕跡。

本文來自:知乎專欄
感謝作者:丁凱
查看原文:Golang協(xié)程調(diào)度(一):協(xié)程狀態(tài)

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

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

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