
狀態(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)