[toc]
- Posted by 微博@Yangsc_o
- 原創(chuàng)文章盯漂,版權(quán)聲明:自由轉(zhuǎn)載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
摘要
使用go語(yǔ)言寫(xiě)程序差不多有半年多了珊燎,也對(duì)go語(yǔ)言有了更深的理解殿怜,今天聊聊go goroutine的調(diào)度原理。
線程
進(jìn)程:進(jìn)程是并發(fā)執(zhí)行程序在執(zhí)行過(guò)程中資源分配和管理的基本單位(資源分配的最小單位)恬叹。進(jìn)程可以理解為一個(gè)應(yīng)用程序的執(zhí)行過(guò)程腹躁,應(yīng)用程序一旦執(zhí)行,就是一個(gè)進(jìn)程募壕。每個(gè)進(jìn)程都有自己獨(dú)立的地址空間调炬,每啟動(dòng)一個(gè)進(jìn)程,系統(tǒng)就會(huì)為它分配地址空間舱馅,建立數(shù)據(jù)表來(lái)維護(hù)代碼段缰泡、堆棧段和數(shù)據(jù)段。
線程:程序執(zhí)行的最小單位代嗤。
并發(fā)編程的目的是為了讓程序運(yùn)行得更快棘钞,但是并不是啟動(dòng)更多的線程就能讓程序最大限度地并發(fā)執(zhí)行。在進(jìn)行并發(fā)編程時(shí)干毅,如果希望通過(guò)多線程執(zhí)行任務(wù)讓程序運(yùn)行得更快宜猜,會(huì)面臨非常多的挑戰(zhàn),比如上下文切換的問(wèn)題硝逢、死鎖的問(wèn)題姨拥,以及受限于硬件和軟件的資源限制問(wèn)題绅喉。
上下文切換
即使是單核CPU也支持多線程執(zhí)行代碼,CPU通過(guò)給每個(gè)線程分配CPU時(shí)間片來(lái)實(shí)現(xiàn)這個(gè)機(jī)制叫乌。時(shí)間片是CPU分配給各個(gè)線程的時(shí)間柴罐,因?yàn)闀r(shí)間片非常短,所以CPU通過(guò)不停地切換線程執(zhí)行憨奸,讓我們感覺(jué)多個(gè)線程時(shí)同時(shí)執(zhí)行的革屠,時(shí)間片一般是幾十毫秒(ms)。
CPU通過(guò)時(shí)間片分配算法來(lái)循環(huán)執(zhí)行任務(wù)排宰,當(dāng)前任務(wù)執(zhí)行一個(gè)時(shí)間片后會(huì)切換到下一個(gè)任務(wù)似芝。但是,在切換前會(huì)保存上一個(gè)任務(wù)的狀態(tài)板甘,以便下次切換回這個(gè)任務(wù)時(shí)国觉,可以再次加載這個(gè)任務(wù)的狀態(tài),從任務(wù)保存到再加載的過(guò)程就是一次上下文切換虾啦。
這就像我們同時(shí)讀兩本書(shū)麻诀,當(dāng)我們?cè)谧x一本英文的技術(shù)書(shū)籍時(shí),發(fā)現(xiàn)某個(gè)單詞不認(rèn)識(shí)傲醉,于是便打開(kāi)中英文詞典蝇闭,但是在放下英文書(shū)籍之前,大腦必須先記住這本書(shū)讀到了多少頁(yè)的第多少行硬毕,等查完單詞之后呻引,能夠繼續(xù)讀這本書(shū)。這樣的切換是會(huì)影響讀書(shū)效率的吐咳,同樣上下文切換也會(huì)影響多線程的執(zhí)行速度逻悠。
在高并發(fā)應(yīng)用中頻繁創(chuàng)建線程會(huì)造成不必要的開(kāi)銷,所以有了線程池韭脊。
線程池
線程池中預(yù)先保存一定數(shù)量的線程童谒,而新任務(wù)將不再以創(chuàng)建線程的方式去執(zhí)行,而是將任務(wù)發(fā)布到任務(wù)隊(duì)列沪羔,線程池中的線程不斷的從任務(wù)隊(duì)列中取 出任務(wù)并執(zhí)行饥伊,可以有效的減少線程創(chuàng)建和銷毀所帶來(lái)的開(kāi)銷。
下圖展示一個(gè)典型的線程池:
G往往代表一個(gè)函數(shù)蔫饰。線程池中的線程worker線程不斷的從任務(wù)隊(duì)列中取出任務(wù)并執(zhí)行琅豆。而worker線程的調(diào)度則交給操作系統(tǒng)進(jìn)行調(diào)度。
如果worker線程執(zhí)行的G任務(wù)中發(fā)生系統(tǒng)調(diào)用篓吁,則操作系統(tǒng)會(huì)將該線程置為阻塞狀態(tài)茫因,也意味著該線程在怠工,也意味著消費(fèi)任務(wù)隊(duì)列的worker線程變少了杖剪,也就是說(shuō)線程池消費(fèi)任務(wù)隊(duì)列的能力變?nèi)趿恕?/p>
如果任務(wù)隊(duì)列中的大部分任務(wù)都會(huì)進(jìn)行系統(tǒng)調(diào)用冻押,則會(huì)讓這種狀態(tài)惡化驰贷,大部分worker線程進(jìn)入阻塞狀態(tài),從而任務(wù)隊(duì)列中的任務(wù)產(chǎn)生堆積翼雀。
解決這個(gè)問(wèn)題的一個(gè)思路就是重新審視線程池中線程的數(shù)量,增加線程池中線程數(shù)量可以一定程度上提高消費(fèi)能力孩擂, 但隨著線程數(shù)量增多狼渊,由于過(guò)多線程爭(zhēng)搶CPU,消費(fèi)能力會(huì)有上限类垦,甚至出現(xiàn)消費(fèi)能力下降狈邑。
這個(gè)問(wèn)題如何解呢?
Goroutine調(diào)度器
線程數(shù)過(guò)多蚤认,意味著操作系統(tǒng)會(huì)不斷的切換線程米苹,頻繁的上下文切換就成了性能瓶頸。Go提供一種機(jī)制砰琢,可以在線程中自己實(shí)現(xiàn)調(diào)度蘸嘶,上下文切換更輕量,從而達(dá)到了線程數(shù)少陪汽,而并發(fā)數(shù)并不少的效果训唱。而線程中調(diào)度的就是 Goroutine.
Goroutine 調(diào)度器的工作就是把“ready-to- run”的goroutine分發(fā)到線程中。
Goroutine主要概念如下:
- G(Goroutine): 即Go協(xié)程挚冤,每個(gè)go關(guān)鍵字都會(huì)創(chuàng)建一個(gè)協(xié)程况增。
- M(Machine): 工作線程,在Go中稱為Machine训挡。
- P(Processor): 處理器(Go中定義的一個(gè)摡念澳骤,不是指CPU),包含運(yùn)行Go代碼的必要資源澜薄,也有調(diào)度 goroutine的能力为肮。
M必須擁有P才可以執(zhí)行G中的代碼,P含有一個(gè)包含多個(gè)G的隊(duì)列肤京,P可以調(diào)度G交由M執(zhí)行弥锄。其關(guān)系如下圖所示:
圖中M是交給操作系統(tǒng)調(diào)度的線程,M持有一個(gè)P蟆沫,P將G調(diào)度進(jìn)M中執(zhí)行籽暇。P同時(shí)還維護(hù)著一個(gè)包含G的隊(duì)列(圖中灰色部 分),可以按照一定的策略將不能的G調(diào)度進(jìn)M中執(zhí)行饭庞。
P的個(gè)數(shù)在程序啟動(dòng)時(shí)決定戒悠,默認(rèn)情況下等同于CPU的核數(shù),由于M必須持有一個(gè)P才可以運(yùn)行Go代碼舟山,所以同時(shí)運(yùn)行的 M個(gè)數(shù)绸狐,也即線程數(shù)一般等同于CPU的個(gè)數(shù)卤恳,以達(dá)到盡可能的使用CPU而又不至于產(chǎn)生過(guò)多的線程切換開(kāi)銷。
程序中可以使用 runtime.GOMAXPROCS() 設(shè)置P的個(gè)數(shù)寒矿,在某些IO密集型的場(chǎng)景下可以在一定程度上提高性能突琳。
Goroutine調(diào)度策略
隊(duì)列輪轉(zhuǎn)
上圖中可見(jiàn)每個(gè)P維護(hù)著一個(gè)包含G的隊(duì)列,不考慮G進(jìn)入系統(tǒng)調(diào)用或IO操作的情況下符相,P周期性的將G調(diào)度到M中執(zhí)行拆融, 執(zhí)行一小段時(shí)間,將上下文保存下來(lái)啊终,然后將G放到隊(duì)列尾部镜豹,然后從隊(duì)列中重新取出一個(gè)G進(jìn)行調(diào)度。
除了每個(gè)P維護(hù)的G隊(duì)列以外蓝牲,還有一個(gè)全局的隊(duì)列趟脂,每個(gè)P會(huì)周期性的查看全局隊(duì)列中是否有G待運(yùn)行并將期調(diào)度到M中執(zhí)行,全局隊(duì)列中G的來(lái)源例衍,主要有從系統(tǒng)調(diào)用中恢復(fù)的G昔期。之所以P會(huì)周期性的查看全局隊(duì)列,也是為了防止全局隊(duì)列中的G被餓死佛玄。
隊(duì)列輪轉(zhuǎn)
上面說(shuō)到P的個(gè)數(shù)默認(rèn)等于CPU核數(shù)镇眷,每個(gè)M必須持有一個(gè)P才可以執(zhí)行G,一般情況下M的個(gè)數(shù)會(huì)略大于P的個(gè)數(shù)翎嫡,這多 出來(lái)的M將會(huì)在G產(chǎn)生系統(tǒng)調(diào)用時(shí)發(fā)揮作用欠动。類似線程池,Go也提供一個(gè)M的池子惑申,需要時(shí)從池子中獲取具伍,用完放回池 子,不夠用時(shí)就再創(chuàng)建一個(gè)圈驼。
如圖所示人芽,當(dāng)G0即將進(jìn)入系統(tǒng)調(diào)用時(shí),M0將釋放P绩脆,進(jìn)而某個(gè)空閑的M1獲取P萤厅,繼續(xù)執(zhí)行P隊(duì)列中剩下的G。而M0由于 陷入系統(tǒng)調(diào)用而進(jìn)被阻塞靴迫,M1接替M0的工作惕味,只要P不空閑,就可以保證充分利用CPU玉锌。
M1的來(lái)源有可能是M的緩存池名挥,也可能是新建的。當(dāng)G0系統(tǒng)調(diào)用結(jié)束后主守,跟據(jù)M0是否能獲取到P禀倔,將會(huì)將G0做不同的處理:
- 如果有空閑的P榄融,則獲取一個(gè)P,繼續(xù)執(zhí)行G0救湖。
- 如果沒(méi)有空閑的P愧杯,則將G0放入全局隊(duì)列,等待被其他的P調(diào)度鞋既。然后M0將進(jìn)入緩存池睡眠
工作量竊取
多個(gè)P中維護(hù)的G隊(duì)列有可能是不均衡的力九,比如下圖:
豎線左側(cè)中右邊的P已經(jīng)將G全部執(zhí)行完,然后去查詢?nèi)株?duì)列涛救,全局隊(duì)列中也沒(méi)有G畏邢,而另一個(gè)M中除了正在運(yùn)行的G 外业扒,隊(duì)列中還有3個(gè)G待運(yùn)行检吆。此時(shí),空閑的P會(huì)將其他P中的G偷取一部分過(guò)來(lái)耀里,一般每次偷取一半莲祸。偷取完如右圖所 示排截。
小結(jié)
一般來(lái)講,程序運(yùn)行時(shí)就將GOMAXPROCS大小設(shè)置為CPU核數(shù)摊灭,可讓Go程序充分利用CPU。在某些IO密集型的應(yīng)用 里败徊,這個(gè)值可能并不意味著性能最好帚呼。理論上當(dāng)某個(gè)Goroutine進(jìn)入系統(tǒng)調(diào)用時(shí),會(huì)有一個(gè)新的M被啟用或創(chuàng)建皱蹦,繼續(xù)占滿CPU煤杀。但由于Go調(diào)度器檢測(cè)到M被阻塞是有一定延遲的,也即舊的M被阻塞和新的M得到運(yùn)行之間是有一定間隔的沪哺,所以在IO密集型應(yīng)用中不妨把GOMAXPROCS設(shè)置的大一些沈自,或許會(huì)有好的效果。
Goroutine池
同理辜妓,在寫(xiě) go 并發(fā)程序的時(shí)候如果程序會(huì)啟動(dòng)大量的 goroutine 枯途,勢(shì)必會(huì)消耗大量的系統(tǒng)資源(內(nèi)存,CPU)籍滴,同理如果引入池化技術(shù)酪夷,衍生出goroutine池,復(fù)用 goroutine 孽惰,則會(huì)節(jié)省資源捶索,提升性能。
選擇一個(gè)開(kāi)源的Ants為例灰瞻。
ants運(yùn)行原理
性能
從該Ants demo 測(cè)試吞吐性能對(duì)比可以看出腥例,使用ants
的吞吐性能相較于原生 goroutine 可以保持在 2-6 倍的性能壓制辅甥,而內(nèi)存消耗則可以達(dá)到 10-20 倍的節(jié)省優(yōu)勢(shì)。
想要深入了解Ants燎竖,請(qǐng)移步項(xiàng)目地址:https://github.com/panjf2000/ants/blob/master/README_ZH.md