Golang 協(xié)程/線程/進(jìn)程 區(qū)別詳解
轉(zhuǎn)載請(qǐng)注明來(lái)源:https://janrs.com/mffp
概念
進(jìn)程 每個(gè)進(jìn)程都有自己的獨(dú)立內(nèi)存空間利职,擁有自己獨(dú)立的地址空間、獨(dú)立的堆和棧植袍,既不共享堆旭愧,亦不共享?xiàng)B拚洹R粋€(gè)程序至少有一個(gè)進(jìn)程勋颖,一個(gè)進(jìn)程至少有一個(gè)線程耘擂。進(jìn)程切換只發(fā)生在內(nèi)核態(tài)摇庙。
線程 線程擁有自己獨(dú)立的棧和共享的堆旱物,共享堆,不共享?xiàng)N捞唬怯刹僮飨到y(tǒng)調(diào)度宵呛,是操作系統(tǒng)調(diào)度(CPU調(diào)度
)執(zhí)行的最小單位。對(duì)于進(jìn)程和線程,都是有內(nèi)核進(jìn)行調(diào)度,有 CPU 時(shí)間片的概念, 進(jìn)行搶占式調(diào)度夕凝。內(nèi)核由系統(tǒng)內(nèi)核進(jìn)行調(diào)度, 系統(tǒng)為了實(shí)現(xiàn)并發(fā),會(huì)不斷地切換線程執(zhí)行, 由此會(huì)帶來(lái)線程的上下文切換宝穗。
協(xié)程 協(xié)程線程一樣共享堆,不共享?xiàng)B氡瑓f(xié)程是由程序員在協(xié)程的代碼中顯示調(diào)度逮矛。協(xié)程(用戶態(tài)線程)是對(duì)內(nèi)核透明的, 也就是系統(tǒng)完全不知道有協(xié)程的存在, 完全由用戶自己的程序進(jìn)行調(diào)度。在棧大小分配方便转砖,且每個(gè)協(xié)程占用的默認(rèn)占用內(nèi)存很小须鼎,只有 2kb
,而線程需要 8mb
堪藐,相較于線程,因?yàn)閰f(xié)程是對(duì)內(nèi)核透明的莉兰,所以棧空間大小可以按需增大減小礁竞。
并發(fā) 多線程程序在單核上運(yùn)行
并行 多線程程序在多核上運(yùn)行
協(xié)程與線程主要區(qū)別是它將不再被內(nèi)核調(diào)度糖荒,而是交給了程序自己而線程是將自己交給內(nèi)核調(diào)度,所以golang中就會(huì)有調(diào)度器的存在模捂。
詳解
進(jìn)程
在計(jì)算機(jī)中捶朵,單個(gè) CPU
架構(gòu)下,每個(gè) CPU
同時(shí)只能運(yùn)行一個(gè)任務(wù)狂男,也就是同時(shí)只能執(zhí)行一個(gè)計(jì)算综看。如果一個(gè)進(jìn)程跑著,就把唯一一個(gè) CPU 給完全占住岖食,顯然是不合理的红碑。而且很大概率上,CPU
被阻塞了,不是因?yàn)橛?jì)算量大析珊,而是因?yàn)榫W(wǎng)絡(luò)阻塞羡鸥。如果此時(shí) CPU
一直被阻塞著,其他進(jìn)程無(wú)法使用忠寻,那么計(jì)算機(jī)資源就是浪費(fèi)了惧浴。
這就出現(xiàn)了多進(jìn)程調(diào)用了。多進(jìn)程就是指計(jì)算機(jī)系統(tǒng)可以同時(shí)執(zhí)行多個(gè)進(jìn)程奕剃,從一個(gè)進(jìn)程到另外一個(gè)進(jìn)程的轉(zhuǎn)換是由操作系統(tǒng)內(nèi)核管理的衷旅,一般是同時(shí)運(yùn)行多個(gè)軟件。
[圖片上傳失敗...(image-2296b3-1685598749508)]
線程
有了多進(jìn)程纵朋,為什么還要線程柿顶?原因如下:
- 進(jìn)程間的信息難以共享數(shù)據(jù),父子進(jìn)程并未共享內(nèi)存操软,需要通過(guò)進(jìn)程間通信(IPC)九串,在進(jìn)程間進(jìn)行信息交換,性能開(kāi)銷(xiāo)較大寺鸥。
- 創(chuàng)建進(jìn)程(一般是調(diào)用 fork 方法)的性能開(kāi)銷(xiāo)較大猪钮。
在一個(gè)進(jìn)程內(nèi),可以設(shè)置多個(gè)執(zhí)行單元胆建,這個(gè)執(zhí)行單元都運(yùn)行在進(jìn)程的上下文中烤低,共享著同樣的代碼和全局?jǐn)?shù)據(jù),由于是在全局共享的笆载,就不存在像進(jìn)程間信息交換的性能損耗扑馁,所以性能和效率就更高了。這個(gè)運(yùn)行在進(jìn)程中的執(zhí)行單元就是線程凉驻。
[圖片上傳失敗...(image-4188a1-1685598749509)]
協(xié)程
官方的解釋?zhuān)?em>鏈接:goroutines說(shuō)明
Goroutines
是使并發(fā)易于使用的一部分腻要。 這個(gè)想法已經(jīng)存在了一段時(shí)間,就是將獨(dú)立執(zhí)行的函數(shù)(協(xié)程)多路復(fù)用到一組線程上涝登。 當(dāng)協(xié)程阻塞時(shí)雄家,例如通過(guò)調(diào)用阻塞系統(tǒng)調(diào)用,運(yùn)行時(shí)會(huì)自動(dòng)將同一操作系統(tǒng)線程上的其他協(xié)程移動(dòng)到不同的可運(yùn)行線程胀滚,這樣它們就不會(huì)被阻塞趟济。 程序員看不到這些,這就是重點(diǎn)咽笼。 我們稱(chēng)之為 goroutines 的結(jié)果可能非常便宜:除了堆棧的內(nèi)存之外顷编,它們的開(kāi)銷(xiāo)很小,只有幾千字節(jié)剑刑。
為了使堆棧變小媳纬,
Go
的運(yùn)行時(shí)使用可調(diào)整大小的有界堆棧。 一個(gè)新創(chuàng)建的goroutine
被賦予幾千字節(jié),這幾乎總是足夠的钮惠。 如果不是杨伙,運(yùn)行時(shí)會(huì)自動(dòng)增加(和縮小)用于存儲(chǔ)堆棧的內(nèi)存萌腿,從而允許許多goroutine
存在于適度的內(nèi)存中。 每個(gè)函數(shù)調(diào)用的CPU
開(kāi)銷(xiāo)平均約為三個(gè)廉價(jià)指令抖苦。 在同一個(gè)地址空間中創(chuàng)建數(shù)十萬(wàn)個(gè)goroutine
是很實(shí)際的毁菱。 如果goroutines
只是線程,系統(tǒng)資源會(huì)以更少的數(shù)量耗盡锌历。
從官方的解釋中可以看到贮庞,協(xié)程是通過(guò)多路復(fù)用到一組線程上,所以本質(zhì)上究西,協(xié)程就是輕量級(jí)的線程窗慎。但是必須要區(qū)分的一點(diǎn)是,協(xié)程是用用戶態(tài)的卤材,進(jìn)程跟線程都是內(nèi)核態(tài)遮斥,這點(diǎn)非常重要,這也是協(xié)程為什么高效的原因扇丛。
協(xié)程的優(yōu)勢(shì)如下:
節(jié)省
CPU
:避免系統(tǒng)內(nèi)核級(jí)的線程頻繁切換术吗,造成的CPU
資源浪費(fèi)。協(xié)程是用戶態(tài)的線程帆精,用戶可以自行控制協(xié)程的創(chuàng)建于銷(xiāo)毀较屿,極大程度避免了系統(tǒng)級(jí)線程上下文切換造成的資源浪費(fèi)。節(jié)約內(nèi)存:在
64
位的Linux
中卓练,一個(gè)線程需要分配8MB
棧內(nèi)存和64MB
堆內(nèi)存隘蝎,系統(tǒng)內(nèi)存的制約導(dǎo)致我們無(wú)法開(kāi)啟更多線程實(shí)現(xiàn)高并發(fā)。而在協(xié)程編程模式下襟企,只需要幾千字節(jié)(執(zhí)行Go協(xié)程只需要極少的棧內(nèi)存嘱么,大概4~5KB,默認(rèn)情況下顽悼,線程棧的大小為1MB
)可以輕松有十幾萬(wàn)協(xié)程拱撵,這是線程無(wú)法比擬的。開(kāi)發(fā)效率:使用協(xié)程在開(kāi)發(fā)程序之中表蝙,可以很方便的將一些耗時(shí)的
IO
操作異步化拴测,例如寫(xiě)文件、耗時(shí)IO
請(qǐng)求等府蛇。并且它們并不是被操作系統(tǒng)所調(diào)度執(zhí)行集索,而是程序員手動(dòng)可以進(jìn)行調(diào)度的。高效率:協(xié)程之間的切換發(fā)生在用戶態(tài),在用戶態(tài)沒(méi)有時(shí)鐘中斷务荆,系統(tǒng)調(diào)用等機(jī)制妆距,因此效率高。
Golang GMP 調(diào)度器
注: 以下相關(guān)知識(shí)摘自劉丹冰(AceLd)的博文:[Golang三關(guān)-典藏版] Golang 調(diào)度器 GMP 原理與調(diào)度全分析
簡(jiǎn)介
-
G
表示:goroutine
函匕,即Go
協(xié)程娱据,每個(gè)go
關(guān)鍵字都會(huì)創(chuàng)建一個(gè)協(xié)程。 -
M
表示:thread
內(nèi)核級(jí)線程盅惜,所有的G
都要放在M
上才能運(yùn)行中剩。 -
P
表示:processor
處理器,調(diào)度G
到M
上抒寂,其維護(hù)了一個(gè)隊(duì)列结啼,存儲(chǔ)了所有需要它來(lái)調(diào)度的G
。
Goroutine
調(diào)度器 P
和 OS
調(diào)度器是通過(guò) M
結(jié)合起來(lái)的屈芜,每個(gè) M
都代表了 1
個(gè)內(nèi)核線程郊愧,OS
調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到 CPU
的核上執(zhí)行,
線程和協(xié)程的映射關(guān)系
在上面的 Golang
官方關(guān)于協(xié)程的解釋中提到:
將獨(dú)立執(zhí)行的函數(shù)(協(xié)程)多路復(fù)用到一組線程上井佑。 當(dāng)協(xié)程阻塞時(shí)属铁,例如通過(guò)調(diào)用阻塞系統(tǒng)調(diào)用,運(yùn)行時(shí)會(huì)自動(dòng)將同一操作系統(tǒng)線程上的其他協(xié)程移動(dòng)到不同的可運(yùn)行線程躬翁,這樣它們就不會(huì)被阻塞红选。
也就是說(shuō),協(xié)程的執(zhí)行是需要通過(guò)線程來(lái)先實(shí)現(xiàn)的姆另。下圖表示的映射關(guān)系:
[圖片上傳失敗...(image-75293-1685598749509)]
在協(xié)程和線程的映射關(guān)系中喇肋,有以下三種:
-
N:1
關(guān)系 -
1:1
關(guān)系 -
M:N
關(guān)系
N:1
關(guān)系
N
個(gè)協(xié)程綁定 1
個(gè)線程,優(yōu)點(diǎn)就是協(xié)程在用戶態(tài)線程即完成切換迹辐,不會(huì)陷入到內(nèi)核態(tài)蝶防,這種切換非常的輕量快速。但也有很大的缺點(diǎn)明吩,1
個(gè)進(jìn)程的所有協(xié)程都綁定在 1
個(gè)線程上间学。
缺點(diǎn):
- 某個(gè)程序用不了硬件的多核加速能力
- 一旦某協(xié)程阻塞,造成線程阻塞印荔,本進(jìn)程的其他協(xié)程都無(wú)法執(zhí)行了低葫,根本就沒(méi)有并發(fā)的能力了。
[圖片上傳失敗...(image-fcb856-1685598749509)]
1:1
關(guān)系
1
個(gè)協(xié)程綁定 1
個(gè)線程仍律,這種最容易實(shí)現(xiàn)嘿悬。協(xié)程的調(diào)度都由 CPU
完成了,不存在 N:1
缺點(diǎn)水泉。
缺點(diǎn):
- 協(xié)程的創(chuàng)建善涨、刪除和切換的代價(jià)都由
CPU
完成窒盐,有點(diǎn)略顯昂貴了。
[圖片上傳失敗...(image-4be2a6-1685598749509)]
M:N
關(guān)系
M
個(gè)協(xié)程綁定 1
個(gè)線程钢拧,是 N:1
和 1:1
類(lèi)型的結(jié)合蟹漓,克服了以上 2
種模型的缺點(diǎn),但實(shí)現(xiàn)起來(lái)最為復(fù)雜源内。
協(xié)程跟線程是有區(qū)別的葡粒,線程由 CPU
調(diào)度是搶占式的,協(xié)程由用戶態(tài)調(diào)度是協(xié)作式的膜钓,一個(gè)協(xié)程讓出 CPU
后嗽交,才執(zhí)行下一個(gè)協(xié)程。
[圖片上傳失敗...(image-ee7480-1685598749509)]
調(diào)度器實(shí)現(xiàn)原理
注:
Go
目前使用的調(diào)度器是2012
年重新設(shè)計(jì)的呻此。
2012
之前的調(diào)度原理,如下圖所示:
[圖片上傳失敗...(image-c25aea-1685598749509)]
M
想要執(zhí)行腔寡、放回 G
都必須訪問(wèn)全局 G
隊(duì)列焚鲜,并且 M
有多個(gè),即多線程訪問(wèn)同一資源需要加鎖進(jìn)行保證互斥 / 同步放前,所以全局 G
隊(duì)列是有互斥鎖進(jìn)行保護(hù)的忿磅。
缺點(diǎn):
- 創(chuàng)建、銷(xiāo)毀凭语、調(diào)度
G
都需要每個(gè)M
獲取鎖葱她,這就形成了激烈的鎖競(jìng)爭(zhēng)。 -
M
轉(zhuǎn)移G
會(huì)造成延遲和額外的系統(tǒng)負(fù)載似扔。比如當(dāng)G
中包含創(chuàng)建新協(xié)程的時(shí)候吨些,M
創(chuàng)建了G
,為了繼續(xù)執(zhí)行G
炒辉,需要把G
交給M
執(zhí)行豪墅,也造成了很差的局部性,因?yàn)?G
和G
是相關(guān)的黔寇,最好放在M
上執(zhí)行偶器,而不是其他M
。 - 系統(tǒng)調(diào)用 (
CPU 在 M 之間的切換
) 導(dǎo)致頻繁的線程阻塞和取消阻塞操作增加了系統(tǒng)開(kāi)銷(xiāo)缝裤。
2012 年之后的調(diào)度器實(shí)現(xiàn)原理屏轰,如下圖所示:
[圖片上傳失敗...(image-1e1a61-1685598749509)]
在新調(diào)度器中,除了 M (thread)
和 G (goroutine)
憋飞,又引進(jìn)了 P (Processor)
霎苗。Processor
,它包含了運(yùn)行 goroutine
的資源榛做,如果線程想運(yùn)行 goroutine
叨粘,必須先獲取 P
猾编,P
中還包含了可運(yùn)行的 G
隊(duì)列。
在 Go
中升敲,線程是運(yùn)行 goroutine
的實(shí)體答倡,調(diào)度器的功能是把可運(yùn)行的 goroutine
分配到工作線程上。調(diào)度過(guò)程如下:
全局隊(duì)列(
Global Queue
):存放等待運(yùn)行的G
驴党。P
的本地隊(duì)列:同全局隊(duì)列類(lèi)似瘪撇,存放的也是等待運(yùn)行的G
,存的數(shù)量有限港庄,不超過(guò)256
個(gè)倔既。新建G
時(shí),G
優(yōu)先加入到P
的本地隊(duì)列鹏氧,如果隊(duì)列滿了渤涌,則會(huì)把本地隊(duì)列中一半的G
移動(dòng)到全局隊(duì)列。P
列表:所有的P
都在程序啟動(dòng)時(shí)創(chuàng)建把还,并保存在數(shù)組中实蓬,最多有GOMAXPROCS
(可配置) 個(gè)。M
:線程想運(yùn)行任務(wù)就得獲取P
吊履,從P
的本地隊(duì)列獲取G
安皱,P
隊(duì)列為空時(shí),M
也會(huì)嘗試從全局隊(duì)列拿一批G
放到P
的本地隊(duì)列艇炎,或從其他P
的本地隊(duì)列偷一半放到自己P
的本地隊(duì)列酌伊。M
運(yùn)行G
,G
執(zhí)行之后缀踪,M
會(huì)從P
獲取下一個(gè)G
居砖,不斷重復(fù)下去。
Goroutine
調(diào)度器和 OS
調(diào)度器是通過(guò) M
結(jié)合起來(lái)的驴娃,每個(gè) M
都代表了 1
個(gè)內(nèi)核線程悯蝉,OS
調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到 CPU
的核上執(zhí)行。
調(diào)度器設(shè)計(jì)策略
復(fù)用線程: 避免頻繁的創(chuàng)建托慨、銷(xiāo)毀線程鼻由,而是對(duì)線程的復(fù)用。
-
work stealing
機(jī)制
當(dāng)本線程無(wú)可運(yùn)行的 G
時(shí)厚棵,嘗試從其他線程綁定的 P
偷取 G
蕉世,而不是銷(xiāo)毀線程。
-
hand off
機(jī)制
當(dāng)本線程因?yàn)?G
進(jìn)行系統(tǒng)調(diào)用阻塞時(shí)婆硬,線程釋放綁定的 P
狠轻,把 P
轉(zhuǎn)移給其他空閑的線程執(zhí)行。
利用并行:
GOMAXPROCS
設(shè)置P
的數(shù)量彬犯,最多有GOMAXPROCS
個(gè)線程分布在多個(gè)CPU
上同時(shí)運(yùn)行向楼。GOMAXPROCS
也限制了并發(fā)的程度查吊,比如GOMAXPROCS = 核數(shù)/2
,則最多利用了一半的CPU
核進(jìn)行并行湖蜕。搶占:在
coroutine
中要等待一個(gè)協(xié)程主動(dòng)讓出CPU
才執(zhí)行下一個(gè)協(xié)程逻卖,在Go
中,一個(gè)goroutine
最多占用CPU
10ms昭抒,防止其他goroutine
被餓死评也,這就是goroutine
不同于coroutine
的一個(gè)地方。全局
G
隊(duì)列:在新的調(diào)度器中依然有全局G
隊(duì)列灭返,但功能已經(jīng)被弱化了盗迟,當(dāng)M
執(zhí)行work stealing
從其他P
偷不到G
時(shí),它可以從全局G
隊(duì)列獲取G
熙含。
go func ()
調(diào)度流程
[圖片上傳失敗...(image-4eed33-1685598749509)]
流程如下:
- 通過(guò)
go func ()
來(lái)創(chuàng)建一個(gè)goroutine
- 有兩個(gè)存儲(chǔ)
G
的隊(duì)列罚缕,一個(gè)是局部調(diào)度器P
的本地隊(duì)列、一個(gè)是全局G
隊(duì)列怎静。新創(chuàng)建的G
會(huì)先保存在P
的本地隊(duì)列中邮弹,如果P
的本地隊(duì)列已經(jīng)滿了就會(huì)保存在全局的隊(duì)列中; -
G
只能運(yùn)行在M
中消约,一個(gè)M
必須持有一個(gè)P
肠鲫,M
與P
是1:1
的關(guān)系员帮。M
會(huì)從P
的本地隊(duì)列彈出一個(gè)可執(zhí)行狀態(tài)的G
來(lái)執(zhí)行或粮,如果P
的本地隊(duì)列為空,就會(huì)向其他的MP
組合偷取一個(gè)可執(zhí)行的G
來(lái)執(zhí)行捞高; - 一個(gè)
M
調(diào)度G
執(zhí)行的過(guò)程是一個(gè)循環(huán)機(jī)制氯材; - 當(dāng)
M
執(zhí)行某一個(gè)G
時(shí)候如果發(fā)生了syscall
或則其余阻塞操作,M
會(huì)阻塞硝岗,如果當(dāng)前有一些G
在執(zhí)行氢哮,runtime
會(huì)把這個(gè)線程M
從P
中摘除 (detach
),然后再創(chuàng)建一個(gè)新的操作系統(tǒng)的線程 (如果有空閑的線程可用就復(fù)用空閑線程) 來(lái)服務(wù)于這個(gè)P
型檀; - 當(dāng)
M
系統(tǒng)調(diào)用結(jié)束時(shí)候冗尤,這個(gè)G
會(huì)嘗試獲取一個(gè)空閑的P
執(zhí)行,并放入到這個(gè)P
的本地隊(duì)列胀溺。如果獲取不到P
裂七,那么這個(gè)線程M
變成休眠狀態(tài), 加入到空閑線程中仓坞,然后這個(gè)G
會(huì)被放入全局隊(duì)列中背零。
調(diào)度器的生命周期
[圖片上傳失敗...(image-ad8cd5-1685598749509)]
特殊的 M0
和 G0
M0
M0
是啟動(dòng)程序后的編號(hào)為 0
的主線程,這個(gè) M
對(duì)應(yīng)的實(shí)例會(huì)在全局變量 runtime.m0
中无埃,不需要在 heap
上分配徙瓶,M0
負(fù)責(zé)執(zhí)行初始化操作和啟動(dòng)第一個(gè) G
毛雇, 在之后 M0
就和其他的 M
一樣了。
G0
G0
是每次啟動(dòng)一個(gè) M
都會(huì)第一個(gè)創(chuàng)建的 goroutine
侦镇,G0
僅用于負(fù)責(zé)調(diào)度的 G
灵疮,G0
不指向任何可執(zhí)行的函數(shù),每個(gè) M
都會(huì)有一個(gè)自己的 G0
虽缕。在調(diào)度或系統(tǒng)調(diào)用時(shí)會(huì)使用 G0
的検寂海空間,全局變量的 G0
是 M0
的 G0
氮趋。
我們來(lái)跟蹤一段代碼:
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
接下來(lái)我們來(lái)針對(duì)上面的代碼對(duì)調(diào)度器里面的結(jié)構(gòu)做一個(gè)分析伍派,也會(huì)經(jīng)歷如上圖所示的過(guò)程:
-
runtime
創(chuàng)建最初的線程m0
和goroutine g0
,并把2
者關(guān)聯(lián)剩胁。 - 調(diào)度器初始化:初始化
m0
诉植、棧、垃圾回收昵观,以及創(chuàng)建和初始化由GOMAXPROCS
個(gè)P
構(gòu)成的P
列表晾腔。 - 示例代碼中的
main
函數(shù)是main.main
,runtime
中也有1
個(gè)main
函數(shù) ——runtime.main
啊犬,代碼經(jīng)過(guò)編譯后灼擂,runtime.main
會(huì)調(diào)用main.main
,程序啟動(dòng)時(shí)會(huì)為runtime.main
創(chuàng)建goroutine
觉至,稱(chēng)它為main goroutine
吧剔应,然后把main goroutine
加入到P
的本地隊(duì)列。 - 啟動(dòng)
m0
语御,m0
已經(jīng)綁定了P
峻贮,會(huì)從P
的本地隊(duì)列獲取G
,獲取到main goroutine
应闯。 -
G
擁有棧纤控,M
根據(jù)G
中的棧信息和調(diào)度信息設(shè)置運(yùn)行環(huán)境。 -
M
運(yùn)行G
碉纺。 -
G
退出船万,再次回到M
獲取可運(yùn)行的G
,這樣重復(fù)下去骨田,直到main.main
退出耿导,runtime.main
執(zhí)行Defer
和Panic
處理,或調(diào)用runtime.exit
退出程序盛撑。
調(diào)度器的生命周期幾乎占滿了一個(gè) Go
程序的一生碎节,runtime.main
的 goroutine
執(zhí)行之前都是為調(diào)度器做準(zhǔn)備工作,runtime.main
的 goroutine
運(yùn)行抵卫,才是調(diào)度器的真正開(kāi)始狮荔,直到 runtime.main
結(jié)束而結(jié)束胎撇。
轉(zhuǎn)載請(qǐng)注明來(lái)源:https://janrs.com/mffp