進(jìn)程、線程恕齐、協(xié)程
進(jìn)程:進(jìn)程是系統(tǒng)進(jìn)行資源分配的基本單位乞娄,有獨(dú)立的內(nèi)存空間,單切換代價(jià)極高,進(jìn)程間通信也比較麻煩
線程:線程是CPU調(diào)度和分派的基本單位仪或,線程依附于進(jìn)程确镊,與其他線程共享進(jìn)程的資源,僅有自己的(程序計(jì)數(shù)器范删,一組寄存器的值蕾域,和棧),線程切換代價(jià)械降(但是線程之間的切換可能會(huì)設(shè)計(jì)用戶態(tài)和內(nèi)核態(tài)的切換)束铭,由于共享進(jìn)程資源,所以線程之間通信比較方便厢绝。
協(xié)程:協(xié)程是一種用戶態(tài)的輕量級(jí)線程契沫,協(xié)程的調(diào)度完全由用戶控制,協(xié)程切換只需要保存和恢復(fù)任務(wù)的上下文昔汉,沒(méi)有內(nèi)核的開(kāi)銷懈万。協(xié)程間通信也比較簡(jiǎn)單(協(xié)程間本身是不可搶占的,由于操作系統(tǒng)的調(diào)度機(jī)制無(wú)法影響到它靶病,因此一般存在用戶自定義的調(diào)度機(jī)制)(也可以這么說(shuō)內(nèi)核線程依然叫“線程(thread)”会通,用戶線程叫“協(xié)程(co-routine)".)
Golang為并發(fā)而生
Goroutine非常輕量,主要體現(xiàn)在以下方面:
- 上下文切換代價(jià)小娄周,Goroutine的上下文切換只涉及到三個(gè)寄存器(PC/SP/DX)的值的修改涕侈,而線程的切換需要涉及模式轉(zhuǎn)換,以及16個(gè)寄存器的刷新煤辨。
- 內(nèi)存占用少裳涛,線程棧空間一般是2M,而goroutine只需要2k;
Go的調(diào)度器實(shí)現(xiàn)機(jī)制
Go程序通過(guò)調(diào)度器來(lái)調(diào)度Goroutine在內(nèi)核級(jí)線程上執(zhí)行众辨,但是并不直接綁定os線程M-Machine運(yùn)行端三,而是由Goroutine Scheduler中的 P-processor作獲取內(nèi)核線程資源的【中介】
Go的調(diào)度器通常被稱為G-M-P模型,實(shí)際包含四個(gè)結(jié)構(gòu)鹃彻,分別為:
G:Goroutine郊闯,每個(gè)Gotoutine對(duì)應(yīng)一個(gè)G結(jié)構(gòu)體,G存儲(chǔ)Goroutine的運(yùn)行堆棧蛛株,狀態(tài)团赁,以及任務(wù)函數(shù),可重用(函數(shù)實(shí)體)G需要保存到P才能被調(diào)度執(zhí)行
M:machine谨履,os內(nèi)核線程抽象欢摄,代表真正執(zhí)行計(jì)算的資源,在綁定有效的P后,進(jìn)入schedule循環(huán)屉符;而shcedule循環(huán)的機(jī)制大致是從Global隊(duì)列剧浸,P的local隊(duì)列以及wait隊(duì)列中獲取锹引。
M的數(shù)量是不固定的矗钟,有Go Runtime調(diào)整唆香,為了防止創(chuàng)建過(guò)多OS線程導(dǎo)致系統(tǒng)調(diào)度不過(guò)來(lái),目前默認(rèn)設(shè)置為10000個(gè)吨艇,M不保存G的上下文躬它,這是G可以跨M的基礎(chǔ)。
P:Processor,表示邏輯處理器东涡,對(duì)G來(lái)說(shuō)冯吓,P相當(dāng)于CPU核,G只有綁定到P才能被調(diào)度疮跑。對(duì)M來(lái)說(shuō)组贺,P提供了相關(guān)的執(zhí)行環(huán)境,入內(nèi)存分配狀態(tài)祖娘,任務(wù)隊(duì)列等失尖。
P 的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的 G 的數(shù)量(前提:物理 CPU 核數(shù) >= P 的數(shù)量)。
P 的數(shù)量由用戶設(shè)置的 GoMAXPROCS 決定渐苏,但是不論 GoMAXPROCS 設(shè)置為多大掀潮,P 的數(shù)量最大為 256。
Sche:Go調(diào)度器琼富,它維護(hù)有存儲(chǔ)M和G的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等仪吧。
調(diào)度器循環(huán)的機(jī)制大致是從各種隊(duì)列、P 的本地隊(duì)列中獲取 G鞠眉,切換到 G 的執(zhí)行棧上并執(zhí)行 G 的函數(shù)薯鼠,調(diào)用 Goexit 做清理工作并回到 M,如此反復(fù)械蹋。
可以通過(guò)經(jīng)典的地鼠推車搬磚的模型來(lái)說(shuō)明其三者關(guān)系:
地鼠(Gopher)的工作任務(wù)是:工地上有若干磚頭人断,地鼠借助小車把磚頭運(yùn)送到火種上去燒制。M 就可以看作圖中的地鼠朝蜘,P 就是小車恶迈,G 就是小車?yán)镅b的磚。
Processor(P):
根據(jù)用戶設(shè)置的 GoMAXPROCS 值來(lái)創(chuàng)建一批小車(P)谱醇。
Goroutine(G):
通過(guò) Go 關(guān)鍵字就是用來(lái)創(chuàng)建一個(gè) Goroutine暇仲,也就相當(dāng)于制造一塊磚(G),然后將這塊磚(G)放入當(dāng)前這輛小車(P)中副渴。
Machine (M):
地鼠(M)不能通過(guò)外部創(chuàng)建出來(lái)奈附,只能磚(G)太多了,地鼠(M)又太少了煮剧,實(shí)在忙不過(guò)來(lái)斥滤,剛好還有空閑的小車(P)沒(méi)有使用将鸵,那就從別處再借些地鼠(M)過(guò)來(lái)直到把小車(P)用完為止。
這里有一個(gè)地鼠(M)不夠用佑颇,從別處借地鼠(M)的過(guò)程顶掉,這個(gè)過(guò)程就是創(chuàng)建一個(gè)內(nèi)核線程(M)。
需要注意的是:地鼠(M) 如果沒(méi)有小車(P)是沒(méi)辦法運(yùn)磚的挑胸,小車(P)的數(shù)量決定了能夠干活的地鼠(M)數(shù)量痒筒,在 Go 程序里面對(duì)應(yīng)的是活動(dòng)線程數(shù);
在Go程序里茬贵,我們也可以通過(guò)下面的圖示來(lái)展示G-M-P模型簿透。
Go 調(diào)度器中有兩個(gè)不同的運(yùn)行隊(duì)列:全局運(yùn)行隊(duì)列(GRQ)和本地運(yùn)行隊(duì)列(LRQ)。
每個(gè) P 都有一個(gè) LRQ解藻,用于管理分配給在 P 的上下文中執(zhí)行的 Goroutines老充,這些 Goroutine 輪流被和 P 綁定的 M 進(jìn)行上下文切換。GRQ 適用于尚未分配給 P 的 Goroutines螟左。
從上圖可以看出啡浊,G 的數(shù)量可以遠(yuǎn)遠(yuǎn)大于 M 的數(shù)量,換句話說(shuō)路狮,Go 程序可以利用少量的內(nèi)核級(jí)線程來(lái)支撐大量 Goroutine 的并發(fā)M:N模型虫啥。多個(gè) Goroutine 通過(guò)用戶級(jí)別的上下文切換來(lái)共享內(nèi)核線程 M 的計(jì)算資源,但對(duì)于操作系統(tǒng)來(lái)說(shuō)并沒(méi)有線程上下文切換產(chǎn)生的性能損耗奄妨。
為了更加充分利用線程的計(jì)算資源涂籽,Go 調(diào)度器采取了以下幾種調(diào)度策略:
任務(wù)竊取:
為了提高 Go 并行處理能力,調(diào)高整體處理效率砸抛,當(dāng)每個(gè) P 之間的 G 任務(wù)不均衡時(shí)评雌,調(diào)度器允許從 GRQ,或者其他 P 的 LRQ 中獲取 G 執(zhí)行直焙。
減少阻塞
在Go里阻塞主要分為以下4個(gè)場(chǎng)景:
1.由于原子景东、互斥量或channel操作調(diào)用導(dǎo)致阻塞,調(diào)度器將把當(dāng)前阻塞的 Goroutine 切換出去奔誓,重新調(diào)度 LRQ 上的其他 Goroutine斤吐。
2.由于網(wǎng)絡(luò)請(qǐng)求和 IO 操作導(dǎo)致 Goroutine 阻塞
Go 程序提供了網(wǎng)絡(luò)輪詢器(NetPoller)來(lái)處理網(wǎng)絡(luò)請(qǐng)求和 IO 操作的問(wèn)題,其后臺(tái)通過(guò) kqueue(MacOS)厨喂,epoll(Linux)或 iocp(Windows)來(lái)實(shí)現(xiàn) IO 多路復(fù)用和措。
通過(guò)使用 NetPoller 進(jìn)行網(wǎng)絡(luò)系統(tǒng)調(diào)用,調(diào)度器可以防止 Goroutine 在進(jìn)行這些系統(tǒng)調(diào)用時(shí)阻塞 M蜕煌。這可以讓 M 執(zhí)行 P 的 LRQ 中其他的 Goroutines派阱,而不需要?jiǎng)?chuàng)建新的 M。有助于減少操作系統(tǒng)上的調(diào)度負(fù)載斜纪。
G1 正在 M 上執(zhí)行贫母,還有 3 個(gè) Goroutine 在 LRQ 上等待執(zhí)行文兑。網(wǎng)絡(luò)輪詢器空閑著,什么都沒(méi)干腺劣。
接下來(lái)绿贞,G1 想要進(jìn)行網(wǎng)絡(luò)系統(tǒng)調(diào)用,因此它被移動(dòng)到網(wǎng)絡(luò)輪詢器并且處理異步網(wǎng)絡(luò)系統(tǒng)調(diào)用誓酒。然后樟蠕,M 可以從 LRQ 執(zhí)行另外的 Goroutine贮聂。此時(shí)靠柑,G2 就被上下文切換到 M 上了。
最后吓懈,異步網(wǎng)絡(luò)系統(tǒng)調(diào)用由網(wǎng)絡(luò)輪詢器完成歼冰,G1 被移回到 P 的 LRQ 中。一旦 G1 可以在 M 上進(jìn)行上下文切換耻警,它負(fù)責(zé)的 Go 相關(guān)代碼就可以再次執(zhí)行隔嫡。這里的最大優(yōu)勢(shì)是,執(zhí)行網(wǎng)絡(luò)系統(tǒng)調(diào)用不需要額外的 M甘穿。網(wǎng)絡(luò)輪詢器使用系統(tǒng)線程腮恩,它時(shí)刻處理一個(gè)有效的事件循環(huán)。
3.當(dāng)調(diào)用一些系統(tǒng)方法的時(shí)候温兼,如果系統(tǒng)方法調(diào)用的時(shí)候發(fā)生阻塞秸滴,這種情況下,網(wǎng)絡(luò)輪詢器(NetPoller)無(wú)法使用募判,而進(jìn)行系統(tǒng)調(diào)用的 Goroutine 將阻塞當(dāng)前 M荡含。
讓我們來(lái)看看同步系統(tǒng)調(diào)用(如文件 I/O)會(huì)導(dǎo)致 M 阻塞的情況:G1 將進(jìn)行同步系統(tǒng)調(diào)用以阻塞 M1。
調(diào)度器介入后:識(shí)別出 G1 已導(dǎo)致 M1 阻塞届垫,此時(shí)释液,調(diào)度器將 M1 與 P 分離,同時(shí)也將 G1 帶走装处。然后調(diào)度器引入新的 M2 來(lái)服務(wù) P误债。此時(shí),可以從 LRQ 中選擇 G2 并在 M2 上進(jìn)行上下文切換妄迁。
阻塞的系統(tǒng)調(diào)用完成后:G1 可以移回 LRQ 并再次由 P 執(zhí)行寝蹈。如果這種情況再次發(fā)生,M1 將被放在旁邊以備將來(lái)重復(fù)使用判族。
4.在Goroutine中去執(zhí)行一個(gè)sleep操作躺盛,導(dǎo)致M被阻塞
Go 程序后臺(tái)有一個(gè)監(jiān)控線程 sysmon,它監(jiān)控那些長(zhǎng)時(shí)間運(yùn)行的 G 任務(wù)然后設(shè)置可以強(qiáng)占的標(biāo)識(shí)符形帮,別的 Goroutine 就可以搶先進(jìn)來(lái)執(zhí)行槽惫。
只要下次這個(gè) Goroutine 進(jìn)行函數(shù)調(diào)用周叮,那么就會(huì)被強(qiáng)占,同時(shí)也會(huì)保護(hù)現(xiàn)場(chǎng)界斜,然后重新放入 P 的本地隊(duì)列里面等待下次執(zhí)行仿耽。