原文: http://morsmachine.dk/go-scheduler
為什么在內(nèi)核的線程調(diào)度器之外Go還需要一個(gè)自己的調(diào)度器?
- POSIX線程API是對(duì)已有的UNIX進(jìn)程模型的邏輯擴(kuò)展,因此線程和進(jìn)程在很多方面都類似回窘。例如策泣,線程有自己的信號(hào)掩碼俺猿,CPU affinity(進(jìn)程要在某個(gè)給定的 CPU 上盡量長時(shí)間地運(yùn)行而不被遷移到其他處理器的傾向性)乔煞,cgroups褂傀。但是有很多特性對(duì)于Go程序來說都是累贅。 2. 另外一個(gè)問題是基于Go語言模型编检,OS的調(diào)度決定并不一定合理胎食。例如,Go的垃圾回收需要內(nèi)存處于一致性的狀態(tài)蒙谓,這需要所有運(yùn)行的線程都停止斥季。垃圾回收的時(shí)間點(diǎn)是不確定的,如果僅由OS來調(diào)度累驮,將會(huì)由大量的線程停止工作酣倾。
- 單獨(dú)開發(fā)一個(gè)Go的調(diào)度器能讓我們知道什么時(shí)候內(nèi)存處于一致性的狀態(tài)。也就是說谤专,當(dāng)開始垃圾回收時(shí)躁锡,運(yùn)行時(shí)只需要為當(dāng)時(shí)正在CPU核上運(yùn)行的那個(gè)線程等待即可,而不是等待所有的線程置侍。
線程模型——高級(jí)語言對(duì)內(nèi)核線程的封裝實(shí)現(xiàn)
- N:1模型映之,N個(gè)用戶空間線程在1個(gè)內(nèi)核空間線程上運(yùn)行。優(yōu)勢(shì)是上下文切換非忱唬快但是無法利用多核系統(tǒng)的優(yōu)點(diǎn)杠输。
- 1:1模型,1個(gè)內(nèi)核空間線程運(yùn)行一個(gè)用戶空間線程秕衙。這種充分利用了多核系統(tǒng)的優(yōu)勢(shì)但是上下文切換非常慢蠢甲,因?yàn)槊恳淮握{(diào)度都會(huì)在用戶態(tài)和內(nèi)核態(tài)之間切換。(POSIX線程模型(pthread)据忘,Java)
- M:N模型鹦牛, 每個(gè)用戶線程對(duì)應(yīng)多個(gè)內(nèi)核空間線程,同時(shí)也可以一個(gè)內(nèi)核空間線程對(duì)應(yīng)多個(gè)用戶空間線程勇吊。Go打算采用這種模型曼追,使用任意個(gè)內(nèi)核模型管理任意個(gè)goroutine。這樣結(jié)合了以上兩種模型的優(yōu)點(diǎn)汉规,但缺點(diǎn)就是調(diào)度的復(fù)雜性礼殊。
Golang的調(diào)度器實(shí)現(xiàn)
Go的調(diào)度器使用了三種結(jié)構(gòu):M,P针史,S
- M代表內(nèi)核線程晶伦,類似于標(biāo)準(zhǔn)的POSIX線程,M代表machine悟民。
- G代表goroutine坝辫,它擁有自己的棧,程序計(jì)數(shù)器(instruction counter)和一些關(guān)于goroutine調(diào)度的信息(如正在阻塞的channel)射亏。
- P代表processor近忙,表示調(diào)度的上下文竭业。可以把它看作是一個(gè)局部的調(diào)度器及舍,讓Go代碼跑在一個(gè)單獨(dú)的線程上未辆。這是讓Go從一個(gè)N:1調(diào)度器映射到一個(gè)M:N調(diào)度器的關(guān)鍵。
如上圖所示锯玛,每個(gè)線程運(yùn)行了一個(gè)goroutine咐柜,所以必須得維持一個(gè)上下文P。
上下文的數(shù)量由啟動(dòng)時(shí)環(huán)境變量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()決定(默認(rèn)值為1攘残,Go1.5以后默認(rèn)值為CPU的核心數(shù))拙友。這意味著在程序執(zhí)行的任意時(shí)刻都只有$GOMAXPROCS個(gè)goroutine在同時(shí)運(yùn)行。
灰色的goroutine沒有在運(yùn)行歼郭,等待被調(diào)度遗契。它們被維護(hù)在一個(gè)隊(duì)列(runqueues)里。當(dāng)一個(gè)go語句執(zhí)行病曾,就將一個(gè)新的goroutine添加到隊(duì)列尾牍蜂;當(dāng)運(yùn)行當(dāng)前goroutine到調(diào)度點(diǎn)時(shí),就從隊(duì)列中彈出一個(gè)新的goroutine泰涂。
每一個(gè)context擁有一個(gè)局部的runqueue鲫竞。之前版本的Go調(diào)度器只有一個(gè)全局的帶有互斥鎖的runqueue,這樣線程經(jīng)常被阻塞等待其它線程解鎖逼蒙,在多核機(jī)器上性能表現(xiàn)及其差从绘。
之所以要維護(hù)多個(gè)context,是因?yàn)楫?dāng)一個(gè)OS線程被阻塞時(shí)其做,我們可以把contex移到其它的線程中去顶考。
如上圖所示赁还,當(dāng)一個(gè)內(nèi)核線程M0要被阻塞時(shí)妖泄,P將會(huì)去M1上繼續(xù)運(yùn)行。Go的調(diào)度器保證了擁有足夠的線程跑所有的contexts艘策。因?yàn)檫€有在執(zhí)行的goroutine蹈胡,M0會(huì)暫時(shí)掛起。當(dāng)syscall返回時(shí)朋蔫,M0會(huì)嘗試獲取一個(gè)context來運(yùn)行G0罚渐。一般情況下,它會(huì)從其它內(nèi)核線程偷一個(gè)過來驯妄。如果沒有偷到荷并,它會(huì)把G0放到一個(gè)全局的runqueue內(nèi),將自己放回線程池青扔,進(jìn)入睡眠狀態(tài)源织。
當(dāng)contexts運(yùn)行完所有的本地runqueue時(shí)翩伪,它會(huì)從全局runqueue拉取goroutine。contexts也會(huì)周期性檢查全局runqueue是否存在goroutine谈息,以防止全局runqueue中的goroutine餓死缘屹。
這就是為什么Go程序多線程運(yùn)行的原因,即使GOMAXPROCS只有1侠仇。
另外一種情況就是某個(gè)context的goroutine運(yùn)行完了轻姿,全局runqueue也沒有了goroutine,而其它c(diǎn)ontext還有大量goroutine需要運(yùn)行逻炊。這時(shí)候就需要從其它的地方獲取goroutine互亮。如圖所示,context會(huì)嘗試從其它c(diǎn)ontext的runqueue里面偷一半的goroutine余素。這樣就能確保所有的線程都能以最大負(fù)荷運(yùn)行胳挎。