以下內(nèi)容均屬個(gè)人理解,如果錯(cuò)誤,還請(qǐng)斧正
What
goroutine是golang中的coroutine,也叫協(xié)程,微軟大法稱之纖程(Fiber)。
協(xié)程是一種更細(xì)粒度的調(diào)度,可以滿足多個(gè)不同處理邏輯的協(xié)程共享一個(gè)線程資源。
Why
在談goroutine之前,先解釋下為什么要使用這種技術(shù):
大家應(yīng)該知道最初操作系統(tǒng)最細(xì)粒度的調(diào)度是內(nèi)核級(jí)線程(Thread),線程其實(shí)就是一個(gè)棧加一堆資源。操作系統(tǒng)一會(huì)將CPU的時(shí)間片分給線程A,一會(huì)將CPU的時(shí)間片分給線程B,靠A和B的棧來保存A和B的執(zhí)行狀態(tài)。起初軟件的并發(fā)處理并不多,線程池完全夠用,但隨著軟件的復(fù)雜度增高,并發(fā)量越來越大,線程成了稀缺資源,所以go發(fā)明了goroutine,提高線程在異步處理中的利用率。
How
golang有一個(gè)強(qiáng)大的調(diào)度器維護(hù)goroutine在內(nèi)核級(jí)線程上運(yùn)行,確保所有的goroutine都使用且盡可能公平的使用CPU資源。支撐整個(gè)調(diào)度器的主要有4個(gè)重要結(jié)構(gòu),分別是M、G、P、Sched:
- M代表內(nèi)核級(jí)線程,一個(gè)M就是一個(gè)線程,goroutine就是跑在M之上的;M是一個(gè)很大的結(jié)構(gòu),里面維護(hù)小對(duì)象內(nèi)存cache(mcache)、當(dāng)前執(zhí)行的goroutine、隨機(jī)數(shù)發(fā)生器等等非常多的信息。
- P全稱是Processor,處理器,它的主要用途就是用來執(zhí)行g(shù)oroutine的,所以它也維護(hù)了一個(gè)goroutine隊(duì)列,里面存儲(chǔ)了所有需要它來執(zhí)行的goroutine,這個(gè)P的角色可能有一點(diǎn)讓人迷惑,一開始容易和M沖突,后面重點(diǎn)聊一下它們的關(guān)系。
- G就是goroutine實(shí)現(xiàn)的核心結(jié)構(gòu)了,G維護(hù)了goroutine需要的棧、程序計(jì)數(shù)器以及它所在的M等信息。
- Sched結(jié)構(gòu)就是調(diào)度器,它維護(hù)有存儲(chǔ)M和G的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等。
網(wǎng)絡(luò)上有一個(gè)圖來比較準(zhǔn)確的描述了M、P和G的關(guān)系:

地鼠用小車運(yùn)著一堆待加工的磚。M就可以看作圖中的地鼠,P就是小車,G就是小車?yán)镅b的磚(以下描述摘錄)。
- runqget, 地鼠(M)試圖從自己的小車(P)取出一塊磚(G),當(dāng)然結(jié)果可能失敗,也就是這個(gè)地鼠的小車已經(jīng)空了,沒有磚了。
- findrunnable, 如果地鼠自己的小車中沒有磚,那也不能閑著不干活是吧,所以地鼠就會(huì)試圖跑去工場(chǎng)倉(cāng)庫(kù)取一塊磚來處理;工場(chǎng)倉(cāng)庫(kù)也可能沒磚啊,出現(xiàn)這種情況的時(shí)候,這個(gè)地鼠也沒有偷懶停下干活,而是悄悄跑出去,隨機(jī)盯上一個(gè)小伙伴(地鼠),然后從它的車?yán)镌噲D偷一半磚到自己車?yán)?。如果多次嘗試偷磚都失敗了,那說明實(shí)在沒有磚可搬了,這個(gè)時(shí)候地鼠就會(huì)把小車還回停車場(chǎng),然后睡覺休息了。如果地鼠睡覺了,下面的過程當(dāng)然都停止了,地鼠睡覺也就是線程sleep了。
- wakep, 到這個(gè)過程的時(shí)候,可憐的地鼠發(fā)現(xiàn)自己小車?yán)镉泻枚啻u啊,自己根本處理不過來;再回頭一看停車場(chǎng)居然有閑置的小車,立馬跑到宿舍一看,你妹,居然還有小伙伴在睡覺,直接給屁股一腳,“你妹,居然還在睡覺,老子都快累死了,趕緊起來干活,分擔(dān)點(diǎn)工作?!保』锇樾蚜?,拿上自己的小車,乖乖干活去了。有時(shí)候,可憐的地鼠跑到宿舍卻發(fā)現(xiàn)沒有在睡覺的小伙伴,于是會(huì)很失望,最后只好向工場(chǎng)老板說——”停車場(chǎng)還有閑置的車啊,我快干不動(dòng)了,趕緊從別的工場(chǎng)借個(gè)地鼠來幫忙吧?!?,最后工場(chǎng)老板就搞來一個(gè)新的地鼠干活了。
- execute,地鼠拿著磚放入火種歡快的燒練起來。
注: “地鼠偷磚”叫work stealing,一種調(diào)度算法。
到這里,貌似整個(gè)工場(chǎng)都正常的運(yùn)轉(zhuǎn)起來了,無懈可擊的樣子。不對(duì),還有一個(gè)疑點(diǎn)沒解決,假設(shè)地鼠的車?yán)镉泻芏啻u,它把一塊磚放入火爐中后,何時(shí)把它取出來,放入第二塊磚呢?難道要一直把第一塊磚燒練好,才取出來嗎?那估計(jì)后面的磚真的是等得花兒都要謝了。這里就是要真正解決goroutine的調(diào)度,上下文切換問題。
調(diào)度點(diǎn):
當(dāng)我們翻看channel的實(shí)現(xiàn)代碼可以發(fā)現(xiàn),對(duì)channel讀寫操作的時(shí)候會(huì)觸發(fā)調(diào)用runtime·park函數(shù)。goroutine調(diào)用park后,這個(gè)goroutine就會(huì)被設(shè)置位waiting狀態(tài),放棄cpu。被park的goroutine處于waiting狀態(tài),并且這個(gè)goroutine不在小車(P)中,如果不對(duì)其調(diào)用runtime·ready,它是永遠(yuǎn)不會(huì)再被執(zhí)行的。除了channel操作外,定時(shí)器中,網(wǎng)絡(luò)poll等都有可能park goroutine。
除了park可以放棄cpu外,調(diào)用runtime·gosched函數(shù)也可以讓當(dāng)前goroutine放棄cpu,但和park完全不同;gosched是將goroutine設(shè)置為runnable狀態(tài),然后放入到調(diào)度器全局等待隊(duì)列(也就是上面提到的工場(chǎng)倉(cāng)庫(kù),這下就明白為何工場(chǎng)倉(cāng)庫(kù)會(huì)有磚塊(G)了吧)。
除此之外,就輪到系統(tǒng)調(diào)用了,有些系統(tǒng)調(diào)用也會(huì)觸發(fā)重新調(diào)度。Go語言完全是自己封裝的系統(tǒng)調(diào)用,所以在封裝系統(tǒng)調(diào)用的時(shí)候,可以做不少手腳,也就是進(jìn)入系統(tǒng)調(diào)用的時(shí)候執(zhí)行entersyscall,退出后又執(zhí)行exitsyscall函數(shù)。 也只有封裝了entersyscall的系統(tǒng)調(diào)用才有可能觸發(fā)重新調(diào)度,它將改變小車(P)的狀態(tài)為syscall。還記一開始提到的sysmon線程嗎?這個(gè)系統(tǒng)監(jiān)控線程會(huì)掃描所有的小車(P),發(fā)現(xiàn)一個(gè)小車(P)處于了syscall的狀態(tài),就知道這個(gè)小車(P)遇到了goroutine在做系統(tǒng)調(diào)用,于是系統(tǒng)監(jiān)控線程就會(huì)創(chuàng)建一個(gè)新的地鼠(M)去把這個(gè)處于syscall的小車給搶過來,開始干活,這樣這個(gè)小車中的所有磚塊(G)就可以繞過之前系統(tǒng)調(diào)用的等待了。被搶走小車的地鼠等系統(tǒng)調(diào)用返回后,發(fā)現(xiàn)自己的車沒,不能繼續(xù)干活了,于是只能把執(zhí)行系統(tǒng)調(diào)用的goroutine放回到工場(chǎng)倉(cāng)庫(kù),自己睡覺去了。
從goroutine的調(diào)度點(diǎn)可以看出,調(diào)度器還是挺粗暴的,調(diào)度粒度有點(diǎn)過大,公平性也沒有想想的那么好。
現(xiàn)場(chǎng)處理:
goroutine在cpu上換入換出,不斷上下文切換的時(shí)候,必須要保證的事情就是保存現(xiàn)場(chǎng)和恢復(fù)現(xiàn)場(chǎng),保存現(xiàn)場(chǎng)就是在goroutine放棄cpu的時(shí)候,將相關(guān)寄存器的值給保存到內(nèi)存中;恢復(fù)現(xiàn)場(chǎng)就是在goroutine重新獲得cpu的時(shí)候,需要從內(nèi)存把之前的寄存器信息全部放回到相應(yīng)寄存器中去。
goroutine在主動(dòng)放棄cpu的時(shí)候(park/gosched),都會(huì)涉及到調(diào)用runtime·mcall函數(shù),此函數(shù)也是匯編實(shí)現(xiàn),主要將goroutine的棧地址和程序計(jì)數(shù)器保存到G結(jié)構(gòu)的sched字段中,mcall就完成了現(xiàn)場(chǎng)保存。恢復(fù)現(xiàn)場(chǎng)的函數(shù)是runtime·gogocall,這個(gè)函數(shù)主要在execute中調(diào)用,就是在執(zhí)行g(shù)oroutine前,需要重新裝載相應(yīng)的寄存器。