[TOC]
[轉(zhuǎn)載]也談goroutine調(diào)度器
本文轉(zhuǎn)載:https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/
Go語言在2016年再次拿下TIBOE年度編程語言稱號(hào),這充分證明了Go語言這幾年在全世界范圍內(nèi)的受歡迎程度。如果要對(duì)世界范圍內(nèi)的gopher發(fā)起一次“你究竟喜歡Go的哪一點(diǎn)”的調(diào)查,我相信很多Gopher會(huì)提到:goroutine。
Goroutine是Go語言原生支持并發(fā)的具體實(shí)現(xiàn),你的Go代碼都無一例外地跑在goroutine中。你可以啟動(dòng)許多甚至成千上萬的goroutine,Go的runtime負(fù)責(zé)對(duì)goroutine進(jìn)行管理。所謂的管理就是“調(diào)度”,粗糙地說調(diào)度就是決定何時(shí)哪個(gè)goroutine將獲得資源開始執(zhí)行、哪個(gè)goroutine應(yīng)該停止執(zhí)行讓出資源、哪個(gè)goroutine應(yīng)該被喚醒恢復(fù)執(zhí)行等。goroutine的調(diào)度是Go team care的事情,大多數(shù)gopher們無需關(guān)心。但個(gè)人覺得適當(dāng)了解一下Goroutine的調(diào)度模型和原理,對(duì)于編寫出更好的go代碼是大有裨益的。因此,在這篇文章中,我將和大家一起來探究一下goroutine調(diào)度器的演化以及模型/原理。
注意:這里要寫的并不是對(duì)goroutine調(diào)度器的源碼分析,國(guó)內(nèi)的雨痕老師在其《Go語言學(xué)習(xí)筆記》一書的下卷“源碼剖析”中已經(jīng)對(duì)Go 1.5.1的scheduler實(shí)現(xiàn)做了細(xì)致且高質(zhì)量的源碼分析了,對(duì)Go scheduler的實(shí)現(xiàn)特別感興趣的gopher可以移步到這本書中去0。這里關(guān)于goroutine scheduler的介紹主要是參考了Go team有關(guān)scheduler的各種design doc、國(guó)外Gopher發(fā)表的有關(guān)scheduler的資料,當(dāng)然雨痕老師的書也給我了很多的啟示。
一、Goroutine調(diào)度器
提到“調(diào)度”,我們首先想到的就是操作系統(tǒng)對(duì)進(jìn)程、線程的調(diào)度。操作系統(tǒng)調(diào)度器會(huì)將系統(tǒng)中的多個(gè)線程按照一定算法調(diào)度到物理CPU上去運(yùn)行。傳統(tǒng)的編程語言比如C、C++等的并發(fā)實(shí)現(xiàn)實(shí)際上就是基于操作系統(tǒng)調(diào)度的,即程序負(fù)責(zé)創(chuàng)建線程(一般通過pthread等lib調(diào)用實(shí)現(xiàn)),操作系統(tǒng)負(fù)責(zé)調(diào)度。這種傳統(tǒng)支持并發(fā)的方式有諸多不足:
- 復(fù)雜
- 創(chuàng)建容易,退出難:做過C/C++ Programming的童鞋都知道,創(chuàng)建一個(gè)thread(比如利用pthread)雖然參數(shù)也不少,但好歹可以接受。但一旦涉及到thread的退出,就要考慮thread是detached,還是需要parent thread去join?是否需要在thread中設(shè)置cancel point,以保證join時(shí)能順利退出?
- 并發(fā)單元間通信困難,易錯(cuò):多個(gè)thread之間的通信雖然有多種機(jī)制可選,但用起來是相當(dāng)復(fù)雜;并且一旦涉及到shared memory,就會(huì)用到各種lock,死鎖便成為家常便飯;
- thread stack size的設(shè)定:是使用默認(rèn)的,還是設(shè)置的大一些,或者小一些呢?
- 難于scaling
- 一個(gè)thread的代價(jià)已經(jīng)比進(jìn)程小了很多了,但我們依然不能大量創(chuàng)建thread,因?yàn)槌嗣總€(gè)thread占用的資源不小之外,操作系統(tǒng)調(diào)度切換thread的代價(jià)也不??;
- 對(duì)于很多網(wǎng)絡(luò)服務(wù)程序,由于不能大量創(chuàng)建thread,就要在少量thread里做網(wǎng)絡(luò)多路復(fù)用,即:使用epoll/kqueue/IoCompletionPort這套機(jī)制,即便有libevent/libev這樣的第三方庫幫忙,寫起這樣的程序也是很不易的,存在大量callback,給程序員帶來不小的心智負(fù)擔(dān)。
為此,Go采用了用戶層輕量級(jí)thread或者說是類coroutine的概念來解決這些問題,Go將之稱為”goroutine“。goroutine占用的資源非常小(Go 1.4將每個(gè)goroutine stack的size默認(rèn)設(shè)置為2k),goroutine調(diào)度的切換也不用陷入(trap)操作系統(tǒng)內(nèi)核層完成,代價(jià)很低。因此,一個(gè)Go程序中可以創(chuàng)建成千上萬個(gè)并發(fā)的goroutine。所有的Go代碼都在goroutine中執(zhí)行,哪怕是go的runtime也不例外。將這些goroutines按照一定算法放到“CPU”上執(zhí)行的程序就稱為goroutine調(diào)度器或goroutine scheduler。
不過,一個(gè)Go程序?qū)τ诓僮飨到y(tǒng)來說只是一個(gè)用戶層程序,對(duì)于操作系統(tǒng)而言,它的眼中只有thread,它甚至不知道有什么叫Goroutine的東西的存在。goroutine的調(diào)度全要靠Go自己完成,實(shí)現(xiàn)Go程序內(nèi)goroutine之間“公平”的競(jìng)爭(zhēng)“CPU”資源,這個(gè)任務(wù)就落到了Go runtime頭上,要知道在一個(gè)Go程序中,除了用戶代碼,剩下的就是go runtime了。
于是Goroutine的調(diào)度問題就演變?yōu)間o runtime如何將程序內(nèi)的眾多goroutine按照一定算法調(diào)度到“CPU”資源上運(yùn)行了。在操作系統(tǒng)層面,Thread競(jìng)爭(zhēng)的“CPU”資源是真實(shí)的物理CPU,但在Go程序?qū)用?,各個(gè)Goroutine要競(jìng)爭(zhēng)的”CPU”資源是什么呢?Go程序是用戶層程序,它本身整體是運(yùn)行在一個(gè)或多個(gè)操作系統(tǒng)線程上的,因此goroutine們要競(jìng)爭(zhēng)的所謂“CPU”資源就是操作系統(tǒng)線程。這樣Go scheduler的任務(wù)就明確了:將goroutines按照一定算法放到不同的操作系統(tǒng)線程中去執(zhí)行。這種在語言層面自帶調(diào)度器的,我們稱之為原生支持并發(fā)。
二、Go調(diào)度器模型與演化過程
1、G-M模型
2012年3月28日,Go 1.0正式發(fā)布。在這個(gè)版本中,Go team實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的調(diào)度器。在這個(gè)調(diào)度器中,每個(gè)goroutine對(duì)應(yīng)于runtime中的一個(gè)抽象結(jié)構(gòu):G,而os thread作為“物理CPU”的存在而被抽象為一個(gè)結(jié)構(gòu):M(machine)。這個(gè)結(jié)構(gòu)雖然簡(jiǎn)單,但是卻存在著許多問題。前Intel blackbelt工程師、現(xiàn)Google工程師Dmitry Vyukov在其《Scalable Go Scheduler Design》一文中指出了G-M模型的一個(gè)重要不足: 限制了Go并發(fā)程序的伸縮性,尤其是對(duì)那些有高吞吐或并行計(jì)算需求的服務(wù)程序。主要體現(xiàn)在如下幾個(gè)方面:
- 單一全局互斥鎖(Sched.Lock)和集中狀態(tài)存儲(chǔ)的存在導(dǎo)致所有g(shù)oroutine相關(guān)操作,比如:創(chuàng)建、重新調(diào)度等都要上鎖;
- goroutine傳遞問題:M經(jīng)常在M之間傳遞”可運(yùn)行”的goroutine,這導(dǎo)致調(diào)度延遲增大以及額外的性能損耗;
- 每個(gè)M做內(nèi)存緩存,導(dǎo)致內(nèi)存占用過高,數(shù)據(jù)局部性較差;
- 由于syscall調(diào)用而形成的劇烈的worker thread阻塞和解除阻塞,導(dǎo)致額外的性能損耗。
2、G-P-M模型
于是Dmitry Vyukov親自操刀改進(jìn)Go scheduler,在Go 1.1中實(shí)現(xiàn)了G-P-M調(diào)度模型和work stealing算法,這個(gè)模型一直沿用至今:
[圖片上傳失敗...(image-1cd72a-1558784715905)]
有名人曾說過:“計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個(gè)間接的中間層來解決”,我覺得Dmitry Vyukov的G-P-M模型恰是這一理論的踐行者。Dmitry Vyukov通過向G-M模型中增加了一個(gè)P,實(shí)現(xiàn)了Go scheduler的scalable。
P是一個(gè)“邏輯Proccessor”,每個(gè)G要想真正運(yùn)行起來,首先需要被分配一個(gè)P(進(jìn)入到P的local runq中,這里暫忽略global runq那個(gè)環(huán)節(jié))。對(duì)于G來說,P就是運(yùn)行它的“CPU”,可以說:G的眼里只有P。但從Go scheduler視角來看,真正的“CPU”是M,只有將P和M綁定才能讓P的runq中G得以真實(shí)運(yùn)行起來。這樣的P與M的關(guān)系,就好比Linux操作系統(tǒng)調(diào)度層面用戶線程(user thread)與核心線程(kernel thread)的對(duì)應(yīng)關(guān)系那樣(N x M)。
3、搶占式調(diào)度
G-P-M模型的實(shí)現(xiàn)算是Go scheduler的一大進(jìn)步,但Scheduler仍然有一個(gè)頭疼的問題,那就是不支持搶占式調(diào)度,導(dǎo)致一旦某個(gè)G中出現(xiàn)死循環(huán)或永久循環(huán)的代碼邏輯,那么G將永久占用分配給它的P和M,位于同一個(gè)P中的其他G將得不到調(diào)度,出現(xiàn)“餓死”的情況。更為嚴(yán)重的是,當(dāng)只有一個(gè)P時(shí)(GOMAXPROCS=1)時(shí),整個(gè)Go程序中的其他G都將“餓死”。于是Dmitry Vyukov又提出了《Go Preemptive Scheduler Design》并在Go 1.2中實(shí)現(xiàn)了“搶占式”調(diào)度。
這個(gè)搶占式調(diào)度的原理則是在每個(gè)函數(shù)或方法的入口,加上一段額外的代碼,讓runtime有機(jī)會(huì)檢查是否需要執(zhí)行搶占調(diào)度。這種解決方案只能說局部解決了“餓死”問題,對(duì)于沒有函數(shù)調(diào)用,純算法循環(huán)計(jì)算的G,scheduler依然無法搶占。
4、NUMA調(diào)度模型
從Go 1.2以后,Go似乎將重點(diǎn)放在了對(duì)GC的低延遲的優(yōu)化上了,對(duì)scheduler的優(yōu)化和改進(jìn)似乎不那么熱心了,只是伴隨著GC的改進(jìn)而作了些小的改動(dòng)。Dmitry Vyukov在2014年9月提出了一個(gè)新的proposal design doc:《NUMA‐aware scheduler for Go》,作為未來Go scheduler演進(jìn)方向的一個(gè)提議,不過至今似乎這個(gè)proposal也沒有列入開發(fā)計(jì)劃。
5、其他優(yōu)化
Go runtime已經(jīng)實(shí)現(xiàn)了netpoller,這使得即便G發(fā)起網(wǎng)絡(luò)I/O操作也不會(huì)導(dǎo)致M被阻塞(僅阻塞G),從而不會(huì)導(dǎo)致大量M被創(chuàng)建出來。但是對(duì)于regular file的I/O操作一旦阻塞,那么M將進(jìn)入sleep狀態(tài),等待I/O返回后被喚醒;這種情況下P將與sleep的M分離,再選擇一個(gè)idle的M。如果此時(shí)沒有idle的M,則會(huì)新創(chuàng)建一個(gè)M,這就是為何大量I/O操作導(dǎo)致大量Thread被創(chuàng)建的原因。
Ian Lance Taylor在Go 1.9 dev周期中增加了一個(gè)Poller for os package的功能,這個(gè)功能可以像netpoller那樣,在G操作支持pollable的fd時(shí),僅阻塞G,而不阻塞M。不過該功能依然不能對(duì)regular file有效,regular file不是pollable的。不過,對(duì)于scheduler而言,這也算是一個(gè)進(jìn)步了。
三、Go調(diào)度器原理的進(jìn)一步理解
1、G、P、M
關(guān)于G、P、M的定義,大家可以參見$GOROOT/src/runtime/runtime2.go這個(gè)源文件。這三個(gè)struct都是大塊兒頭,每個(gè)struct定義都包含十幾個(gè)甚至二、三十個(gè)字段。像scheduler這樣的核心代碼向來很復(fù)雜,考慮的因素也非常多,代碼“耦合”成一坨。不過從復(fù)雜的代碼中,我們依然可以看出來G、P、M的各自大致用途(當(dāng)然雨痕老師的源碼分析功不可沒),這里簡(jiǎn)要說明一下:
- G: 表示goroutine,存儲(chǔ)了goroutine的執(zhí)行stack信息、goroutine狀態(tài)以及goroutine的任務(wù)函數(shù)等;另外G對(duì)象是可以重用的。
- P: 表示邏輯processor,P的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的G的數(shù)量(前提:系統(tǒng)的物理cpu核數(shù)>=P的數(shù)量);P的最大作用還是其擁有的各種G對(duì)象隊(duì)列、鏈表、一些cache和狀態(tài)。
- M: M代表著真正的執(zhí)行計(jì)算資源。在綁定有效的p后,進(jìn)入schedule循環(huán);而schedule循環(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ǔ)。
下面是G、P、M定義的代碼片段:
//src/runtime/runtime2.go
type g struct {
stack stack // offset known to runtime/cgo
sched gobuf
goid int64
gopc uintptr // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function
... ...
}
type p struct {
lock mutex
id int32
status uint32 // one of pidle/prunning/...
mcache *mcache
racectx uintptr
// Queue of runnable goroutines. Accessed without lock.
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr
// Available G's (status == Gdead)
gfree *g
gfreecnt int32
... ...
}
type m struct {
g0 *g // goroutine with scheduling stack
mstartfn func()
curg *g // current running goroutine
.... ..
}
2、G被搶占調(diào)度
和操作系統(tǒng)按時(shí)間片調(diào)度線程不同,Go并沒有時(shí)間片的概念。如果某個(gè)G沒有進(jìn)行system call調(diào)用、沒有進(jìn)行I/O操作、沒有阻塞在一個(gè)channel操作上,那么m是如何讓G停下來并調(diào)度下一個(gè)runnable G的呢?答案是:G是被搶占調(diào)度的。
前面說過,除非極端的無限循環(huán)或死循環(huán),否則只要G調(diào)用函數(shù),Go runtime就有搶占G的機(jī)會(huì)。Go程序啟動(dòng)時(shí),runtime會(huì)去啟動(dòng)一個(gè)名為sysmon的m(一般稱為監(jiān)控線程),該m無需綁定p即可運(yùn)行,該m在整個(gè)Go程序的運(yùn)行過程中至關(guān)重要:
//$GOROOT/src/runtime/proc.go
// The main goroutine.
func main() {
... ...
systemstack(func() {
newm(sysmon, nil)
})
.... ...
}
// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
// If a heap span goes unused for 5 minutes after a garbage collection,
// we hand it back to the operating system.
scavengelimit := int64(5 * 60 * 1e9)
... ...
if .... {
... ...
// retake P's blocked in syscalls
// and preempt long running G's
if retake(now) != 0 {
idle = 0
} else {
idle++
}
... ...
}
}
sysmon每20us~10ms啟動(dòng)一次,按照《Go語言學(xué)習(xí)筆記》中的總結(jié),sysmon主要完成如下工作:
- 釋放閑置超過5分鐘的span物理內(nèi)存;
- 如果超過2分鐘沒有垃圾回收,強(qiáng)制執(zhí)行;
- 將長(zhǎng)時(shí)間未處理的netpoll結(jié)果添加到任務(wù)隊(duì)列;
- 向長(zhǎng)時(shí)間運(yùn)行的G任務(wù)發(fā)出搶占調(diào)度;
- 收回因syscall長(zhǎng)時(shí)間阻塞的P;
我們看到sysmon將“向長(zhǎng)時(shí)間運(yùn)行的G任務(wù)發(fā)出搶占調(diào)度”,這個(gè)事情由retake實(shí)施:
// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms
func retake(now int64) uint32 {
... ...
// Preempt G if it's running for too long.
t := int64(_p_.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
continue
}
if pd.schedwhen+forcePreemptNS > now {
continue
}
preemptone(_p_)
... ...
}
可以看出,如果一個(gè)G任務(wù)運(yùn)行10ms,sysmon就會(huì)認(rèn)為其運(yùn)行時(shí)間太久而發(fā)出搶占式調(diào)度的請(qǐng)求。一旦G的搶占標(biāo)志位被設(shè)為true,那么待這個(gè)G下一次調(diào)用函數(shù)或方法時(shí),runtime便可以將G搶占,并移出運(yùn)行狀態(tài),放入P的local runq中,等待下一次被調(diào)度。
3、channel阻塞或network I/O情況下的調(diào)度
如果G被阻塞在某個(gè)channel操作或network I/O操作上時(shí),G會(huì)被放置到某個(gè)wait隊(duì)列中,而M會(huì)嘗試運(yùn)行下一個(gè)runnable的G;如果此時(shí)沒有runnable的G供m運(yùn)行,那么m將解綁P,并進(jìn)入sleep狀態(tài)。當(dāng)I/O available或channel操作完成,在wait隊(duì)列中的G會(huì)被喚醒,標(biāo)記為runnable,放入到某P的隊(duì)列中,綁定一個(gè)M繼續(xù)執(zhí)行。
4、system call阻塞情況下的調(diào)度
如果G被阻塞在某個(gè)system call操作上,那么不光G會(huì)阻塞,執(zhí)行該G的M也會(huì)解綁P(實(shí)質(zhì)是被sysmon搶走了),與G一起進(jìn)入sleep狀態(tài)。如果此時(shí)有idle的M,則P與其綁定繼續(xù)執(zhí)行其他G;如果沒有idle M,但仍然有其他G要去執(zhí)行,那么就會(huì)創(chuàng)建一個(gè)新M。
當(dāng)阻塞在syscall上的G完成syscall調(diào)用后,G會(huì)去嘗試獲取一個(gè)可用的P,如果沒有可用的P,那么G會(huì)被標(biāo)記為runnable,之前的那個(gè)sleep的M將再次進(jìn)入sleep。
四、調(diào)度器狀態(tài)的查看方法
Go提供了調(diào)度器當(dāng)前狀態(tài)的查看方法:使用Go運(yùn)行時(shí)環(huán)境變量GODEBUG。
$GODEBUG=schedtrace=1000 godoc -http=:6060
SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0]
SCHED 1001ms: gomaxprocs=4 idleprocs=0 threads=9 spinningthreads=0 idlethreads=3 runqueue=2 [8 14 5 2]
SCHED 2006ms: gomaxprocs=4 idleprocs=0 threads=25 spinningthreads=0 idlethreads=19 runqueue=12 [0 0 4 0]
SCHED 3006ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=8 runqueue=2 [0 1 1 0]
SCHED 4010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=12 [6 3 1 0]
SCHED 5010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=1 idlethreads=20 runqueue=17 [0 0 0 0]
SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10]
... ...
GODEBUG這個(gè)Go運(yùn)行時(shí)環(huán)境變量很是強(qiáng)大,通過給其傳入不同的key1=value1,key2=value2… 組合,Go的runtime會(huì)輸出不同的調(diào)試信息,比如在這里我們給GODEBUG傳入了”schedtrace=1000″,其含義就是每1000ms,打印輸出一次goroutine scheduler的狀態(tài),每次一行。每一行各字段含義如下:
以上面例子中最后一行為例:
SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10]
SCHED:調(diào)試信息輸出標(biāo)志字符串,代表本行是goroutine scheduler的輸出;
6016ms:即從程序啟動(dòng)到輸出這行日志的時(shí)間;
gomaxprocs: P的數(shù)量;
idleprocs: 處于idle狀態(tài)的P的數(shù)量;通過gomaxprocs和idleprocs的差值,我們就可知道執(zhí)行g(shù)o代碼的P的數(shù)量;
threads: os threads的數(shù)量,包含scheduler使用的m數(shù)量,加上runtime自用的類似sysmon這樣的thread的數(shù)量;
spinningthreads: 處于自旋狀態(tài)的os thread數(shù)量;
idlethread: 處于idle狀態(tài)的os thread的數(shù)量;
runqueue=1: go scheduler全局隊(duì)列中G的數(shù)量;
[3 4 0 10]: 分別為4個(gè)P的local queue中的G的數(shù)量。
我們還可以輸出每個(gè)goroutine、m和p的詳細(xì)調(diào)度信息,但對(duì)于Go user來說,絕大多數(shù)時(shí)間這是不必要的:
$ GODEBUG=schedtrace=1000,scheddetail=1 godoc -http=:6060
SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0
P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=17
M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1
G1: status=8() m=0 lockedm=0
G17: status=3() m=1 lockedm=1
SCHED 1002ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=6 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
P0: status=2 schedtick=2293 syscalltick=18928 m=-1 runqsize=12 gfreecnt=2
P1: status=1 schedtick=2356 syscalltick=19060 m=11 runqsize=11 gfreecnt=0
P2: status=2 schedtick=2482 syscalltick=18316 m=-1 runqsize=37 gfreecnt=1
P3: status=2 schedtick=2816 syscalltick=18907 m=-1 runqsize=2 gfreecnt=4
M12: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
M11: p=1 curg=6160 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
... ...
SCHED 2002ms: gomaxprocs=4 idleprocs=0 threads=23 spinningthreads=0 idlethreads=5 runqueue=4 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
P0: status=0 schedtick=2972 syscalltick=29458 m=-1 runqsize=0 gfreecnt=6
P1: status=2 schedtick=2964 syscalltick=33464 m=-1 runqsize=0 gfreecnt=39
P2: status=1 schedtick=3415 syscalltick=33283 m=18 runqsize=0 gfreecnt=12
P3: status=2 schedtick=3736 syscalltick=33701 m=-1 runqsize=1 gfreecnt=6
M22: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
M21: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
... ...
關(guān)于go scheduler調(diào)試信息輸出的詳細(xì)信息,可以參考Dmitry Vyukov的大作:《Debugging performance issues in Go programs》。這也應(yīng)該是每個(gè)gopher必讀的經(jīng)典文章。當(dāng)然更詳盡的代碼可參考$GOROOT/src/runtime/proc.go中的schedtrace函數(shù)。