要理解甚么是goroutine,我們先來(lái)看看進(jìn)程、線程和協(xié)程它們之間的區(qū)分,這能幫助我們更好的理解goroutine。
進(jìn)程:分配完全獨(dú)立的地址空間,具有自己獨(dú)立的堆和棧,既不同享堆,亦不同享?xiàng)#M(jìn)程的切換只產(chǎn)生在內(nèi)核態(tài),由操作系統(tǒng)調(diào)度。
線程:和其它本進(jìn)程的線程同享地址空間,具有自己獨(dú)立的棧和同享的堆,同享堆,不同享?xiàng)#€程的切換1般也由操作系統(tǒng)調(diào)度(標(biāo)準(zhǔn)線程是的)。
協(xié)程:和線程類似,同享堆,不同享?xiàng)#瑓f(xié)程的切換1般由程序員在代碼中顯式控制。
進(jìn)程和線程的切換主要依賴于時(shí)間片的控制(關(guān)于進(jìn)程和線程的調(diào)度方式,具體可參看這篇文章:http://blog.chinaunix.net/uid⑵0476365-id⑴942505.html),而協(xié)程的切換則主要依賴于本身,這樣的好處是避免了無(wú)意義的調(diào)度,由此可以提高性能,但也因此,程序員必須自己承當(dāng)調(diào)度的責(zé)任。
goroutine可以看做是協(xié)程的go語(yǔ)言實(shí)現(xiàn),從百度百科上看協(xié)程的定義:與子例程1樣,協(xié)程(coroutine)也是1種程序組件。相對(duì)子例程而言,協(xié)程更加1般和靈活,但在實(shí)踐中使用沒(méi)有子例程那樣廣泛。實(shí)際上,我們可以把子例程當(dāng)作是協(xié)程的1種特例。1般來(lái)講,如果沒(méi)有顯式的讓出CPU,就會(huì)1直履行當(dāng)前協(xié)程。
我們知道goroutine是協(xié)程的go語(yǔ)言實(shí)現(xiàn),它是語(yǔ)言原生支持的,相對(duì)1般由庫(kù)實(shí)現(xiàn)協(xié)程的方式,goroutine更加強(qiáng)大,它的調(diào)度1定程度上是由go運(yùn)行時(shí)(runtime)管理。其好處之1是,當(dāng)某goroutine產(chǎn)生阻塞時(shí)(例猶如步IO操作等),會(huì)自動(dòng)出讓CPU給其它goroutine。
goroutine的使用非常簡(jiǎn)單,例如foo是1個(gè)函數(shù):
go foo()
就1個(gè)關(guān)鍵字go弄定了,這里會(huì)啟動(dòng)1個(gè)goroutine履行foo函數(shù),然后CPU繼續(xù)履行后面的代碼。這里雖然啟動(dòng)了goroutine,但其實(shí)不意味著它會(huì)得到馬上調(diào)度,關(guān)于goroutine的調(diào)度我們稍后再探討。
goroutine是非常輕量級(jí)的,它就是1段代碼,1個(gè)函數(shù)入口,和在堆上為其分配的1個(gè)堆棧(初始大小為4K,會(huì)隨著程序的履行自動(dòng)增長(zhǎng)刪除)。所以它非常便宜,我們可以很輕松的創(chuàng)建上萬(wàn)個(gè)goroutine。
默許的, 所有g(shù)oroutine會(huì)在1個(gè)原生線程里跑,也就是只使用了1個(gè)CPU核。在同1個(gè)原生線程里,如果當(dāng)前goroutine不產(chǎn)生阻塞,它是不會(huì)讓出CPU時(shí)間給其他同線程的goroutines的。除被系統(tǒng)調(diào)用阻塞的線程外,Go運(yùn)行庫(kù)最多會(huì)啟動(dòng)$GOMAXPROCS個(gè)線程來(lái)運(yùn)行g(shù)oroutine。
那末goroutine究竟是如何被調(diào)度的呢?我們從go程序啟動(dòng)開(kāi)始說(shuō)起。在go程序啟動(dòng)時(shí)會(huì)首先創(chuàng)建1個(gè)特殊的內(nèi)核線程sysmon,從名字就能夠看出來(lái)它的職責(zé)是負(fù)責(zé)監(jiān)控的,goroutine背后的調(diào)度可以說(shuō)就是靠它來(lái)弄定。
接下來(lái),我們?cè)倏纯此恼{(diào)度模型,go語(yǔ)言當(dāng)前的實(shí)現(xiàn)是N:M。即1定數(shù)量的用戶線程映照到1定數(shù)量的OS線程上,這里的用戶線程在go中指的就是goroutine。go語(yǔ)言的調(diào)度模型需要弄清楚3個(gè)概念:M、P和G,以下圖表示:
M代表OS線程,G代表goroutine,P的概念比較重要,它表示履行的上下文,其數(shù)量由$GOMAXPROCS決定,1般來(lái)講正好等于處理器的數(shù)量。M必須和P綁定才能履行G,調(diào)度器需要保證所有的P都有G履行,以保證并行度。以下圖:
從圖中我們可以看見(jiàn),當(dāng)前有兩個(gè)P,各自綁定了1個(gè)M,并分別履行了1個(gè)goroutine,我們還可以看見(jiàn)每一個(gè)P上還掛了1個(gè)G的隊(duì)列,這個(gè)隊(duì)列是代表私有的任務(wù)隊(duì)列,它們實(shí)際上都是runnable狀態(tài)的goroutine。當(dāng)使用go關(guān)鍵字聲明時(shí),1個(gè)goroutine便被加入到運(yùn)行隊(duì)列的尾部。1旦1個(gè)goroutine運(yùn)行到1個(gè)調(diào)度點(diǎn),上下文便從運(yùn)行隊(duì)列中取出1個(gè)goroutine, 設(shè)置好棧和指令指針,便開(kāi)始運(yùn)行新的goroutine。
那末go中切換goroutine的調(diào)度點(diǎn)有哪些呢?具體有以下3種情況
調(diào)度點(diǎn)的情況說(shuō)清楚了,但全部模型還其實(shí)不完全。我們知道當(dāng)使用go去調(diào)用1個(gè)函數(shù),會(huì)生成1個(gè)新的goroutine放入當(dāng)前P的隊(duì)列中,那末甚么時(shí)候生成別的OS線程,各個(gè)OS線程又是如何做負(fù)載均衡的呢?
當(dāng)M從隊(duì)列中拿到1個(gè)可履行的G后,首先會(huì)去檢查1下,自己的隊(duì)列中是不是還有等待的G,如果還有等待的G,并且也還有空閑的P,此時(shí)就會(huì)通知runtime分配1個(gè)新的M(如果有在睡覺(jué)的OS線程,則直接喚醒它,沒(méi)有的話則生成1個(gè)新的OS線程)來(lái)分擔(dān)負(fù)務(wù)。
如果某個(gè)M發(fā)現(xiàn)隊(duì)列為空以后,會(huì)首先從全局隊(duì)列中取1個(gè)G來(lái)處理。如果全局隊(duì)列也空了,則會(huì)隨機(jī)從別的P那里直接截取1半的隊(duì)列過(guò)來(lái)(偷竊任務(wù)),如果發(fā)現(xiàn)所有的P都沒(méi)有可供偷竊的G了,該M就會(huì)墮入沉睡。
全部調(diào)度模型大致就是這模樣了,和所有協(xié)程的調(diào)度1樣,在響應(yīng)時(shí)間上,這類協(xié)作式調(diào)度是硬傷。很容易致使某個(gè)協(xié)程長(zhǎng)時(shí)間沒(méi)法得到履行。但整體來(lái)講,它帶來(lái)的好處更加讓人驚嘆。想要了解的更多可以看看我下面列出的1些參考資料,或是直接看它的源碼:http://golang.org/src/runtime/proc.c
總綱傳送門(mén):golang技術(shù)隨筆總綱