Go 并發(fā)實(shí)戰(zhàn)--協(xié)程淺析 一

前言

在說go協(xié)程之前,先對比看一下進(jìn)程&線程&協(xié)程這幾個(gè)基礎(chǔ)的概念。
進(jìn)程是指一段程序的執(zhí)行過程,具有自己的地址空間(包括文本區(qū)域(text region)、數(shù)據(jù)區(qū)域(data region)和堆棧(stack region)),并且進(jìn)程由cpu直接負(fù)責(zé)調(diào)度控制。
線程是CPU調(diào)度的最小單位,線程只是一個(gè)進(jìn)程中的不同執(zhí)行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨(dú)的地址空間。同樣是由cpu直接負(fù)責(zé)調(diào)度控制的。
協(xié)程可以理解為是用戶級線程,對于協(xié)程來說對內(nèi)核透明的,也就是系統(tǒng)并不知道有協(xié)程的存在,是完全由用戶自己的程序進(jìn)行調(diào)度的,cpu對于我們的協(xié)程無感知。
goroutine實(shí)際上就是協(xié)程,為什么叫做go協(xié)程呢,因?yàn)間o在runtime、系統(tǒng)調(diào)用方面對goroutine調(diào)度進(jìn)行了封裝和處理,也就是說go在語言層面實(shí)現(xiàn)對于go協(xié)程的支持:使用go 關(guān)鍵字就可以了。
內(nèi)存消耗方面:
每個(gè) goroutine (協(xié)程) 默認(rèn)占用內(nèi)存遠(yuǎn)比 Java 、C 的線程少。
goroutine:2KB
線程:8MB
線程和 goroutine 切換調(diào)度開銷方面:
線程/goroutine 切換開銷方面,goroutine 遠(yuǎn)比線程小
線程:涉及模式切換(從用戶態(tài)切換到內(nèi)核態(tài))、16個(gè)寄存器、PC、SP...等寄存器的刷新等。
goroutine:只有三個(gè)寄存器的值修改 - PC / SP / DX.
最主要的是不擔(dān)心協(xié)程間切換、或者協(xié)程打滿或者夯死。
關(guān)于協(xié)程協(xié)程這類知識(shí),感覺先說原理再說使用會(huì)比較理解,后面就先來看下go協(xié)程的實(shí)現(xiàn)原理。

協(xié)程實(shí)現(xiàn)原理

線程當(dāng)前的問題主要是線程切換及夯死的問題,于是操作系統(tǒng)提供了基于事件模式的異步編程模型,用少量的線程來服務(wù)大量的網(wǎng)絡(luò)連接和I/O操作。但是采用異步和基于事件的編程模型,復(fù)雜化了程序代碼的編寫,非常容易出錯(cuò)。
而協(xié)程實(shí)際上就是幫我們解決了這個(gè)問題,在應(yīng)用層模擬的線程,他避免了上下文切換的額外耗費(fèi),兼顧了多線程的優(yōu)點(diǎn)。簡化了高并發(fā)程序的復(fù)雜度。也就是說協(xié)程幫我們封裝了少量的線程來服務(wù)大量的網(wǎng)絡(luò)連接和I/O操作,和線程之間的切換及服務(wù)對應(yīng)服務(wù)對象的邏輯。
比如說:一個(gè)socket鏈接對應(yīng)一個(gè)協(xié)程來處理,而這個(gè)協(xié)程具體是由哪個(gè)線程來處理及當(dāng)前的是否需要處理由go來決定,這樣編程就變的十分簡單,并且很大程度上避免了切換的問題,也不怕一個(gè)socket夯死在那里了。

go協(xié)程實(shí)現(xiàn)機(jī)制

下面來看一下go協(xié)程的實(shí)現(xiàn)原理,go協(xié)程主要有g(shù)oroutine、machine、process這幾個(gè)核心概念,go其實(shí)就是在這幾個(gè)概念的基礎(chǔ)上搭建了一個(gè)go協(xié)程到cpu的調(diào)度體系。


image.png
goroutine

goroutine 就是我們使用的go協(xié)程,使用go 關(guān)鍵字就可以了:go func1(),所有的go協(xié)程都由runtime管理(新建、恢復(fù)、停止、休眠,其中執(zhí)行異步操作時(shí)goroutine會(huì)陷入休眠,不占用系統(tǒng)線程(這是go協(xié)程很方便的一點(diǎn)),當(dāng)新建或者恢復(fù)時(shí)加到任務(wù)隊(duì)列中)
routine狀態(tài):
空閑中(idle): 新建,但未初始化
待運(yùn)行(runnable): 在運(yùn)行隊(duì)列中, 等待M取出并運(yùn)行
運(yùn)行中(running): 表示machine正在執(zhí)行這個(gè)routine
系統(tǒng)調(diào)用中(syscall): 正在運(yùn)行的routine發(fā)起的系統(tǒng)調(diào)用
等待中(waiting): 在等待某些條件完成,不在執(zhí)行也不在運(yùn)行隊(duì)列中(可能在channel的等待隊(duì)列中)
已中止(dead): 未被使用或可能已執(zhí)行完畢
棧復(fù)制中(copystack): 正在獲取一個(gè)新的??臻g并把原來的內(nèi)容復(fù)制過去(用于防止GC掃描)

machine

machine指的是系統(tǒng)線程,他是go協(xié)程中代碼的具體執(zhí)行者,它執(zhí)行:
1、goroutine需要執(zhí)行的代碼(需要process)
2、原生代碼(不需要process)
machine會(huì)從goroutine運(yùn)行隊(duì)列中取出一個(gè)goroutine來執(zhí)行,當(dāng)執(zhí)行完或者陷入休眠時(shí),取一個(gè)goroutine繼續(xù)執(zhí)行,但是有些時(shí)候goroutine會(huì)調(diào)用一些原生代碼會(huì)產(chǎn)生阻塞行為,但是無法陷入休眠,這個(gè)時(shí)候machine會(huì)陷入阻塞,但是會(huì)方式該goroutine持有的process,其他的goroutine能夠拿到這個(gè)process來運(yùn)行其他的goroutine。
這里的machine可以簡單粗暴的理解為就是Java中的線程,而goroutine其實(shí)就是go為我們做的一層封裝,其中的對應(yīng)關(guān)系就是go所實(shí)現(xiàn)的調(diào)度策略。
如果是線程的話,不難理解,machine的數(shù)量需要保證不能太少,也不能太多,需要讓cpu時(shí)刻在工作,并且不能頻繁切換。
自旋(spinning): 正在取goroutine
執(zhí)行態(tài)(running): 在執(zhí)行代碼或者阻塞的syscall
休眠中 : 當(dāng)上個(gè)任務(wù)執(zhí)行完,但goroutine隊(duì)列中無任務(wù)時(shí)

process

上面提到了process這個(gè)概念,process指的就是machine執(zhí)行g(shù)oroutine時(shí)所用到的資源,這里可以看出process僅與goroutine相關(guān),與原生代碼無關(guān)。一個(gè)machine執(zhí)行一個(gè)goroutine的代碼時(shí),強(qiáng)依賴于process,所以說如果僅有一個(gè)process,同一時(shí)刻僅有一個(gè)machine可以執(zhí)行,如果有n(n小于可用cpu核數(shù))個(gè),同一時(shí)刻可以有n個(gè)machine可以執(zhí)行。
process中的數(shù)據(jù)是lock free的,效率非常高。
process的數(shù)量通常等于cpu核數(shù),但是兩者并沒有直接的對應(yīng)關(guān)系,僅僅是process通常和同一時(shí)刻最多執(zhí)行的machine數(shù)量相同,而同一時(shí)刻最多執(zhí)行的machine也就是cpu核數(shù)罷了。
空閑中(idle): 沒有被用到的process所處的狀態(tài)
運(yùn)行中(running): 正在執(zhí)行的machine所持有process
系統(tǒng)調(diào)用中syscall): 當(dāng)goroutine調(diào)用原生代碼, 原生代碼又反過來調(diào)用go代碼時(shí)
GC停止中(gcstop): 當(dāng)gc stop the world時(shí)
已中止(dead): 當(dāng)P的數(shù)量在運(yùn)行時(shí)改變, 且數(shù)量減少時(shí),多余的process的狀態(tài)

各種隊(duì)列

待執(zhí)行隊(duì)列:
正在待執(zhí)行的goroutine是存在于待執(zhí)行隊(duì)列中的,而待執(zhí)行隊(duì)列又分為本地待執(zhí)行隊(duì)列全局待執(zhí)行隊(duì)列,加入時(shí)會(huì)優(yōu)先加入本地待執(zhí)行隊(duì)列,取goroutine時(shí)也會(huì)優(yōu)先取本地待執(zhí)行隊(duì)列。本地待執(zhí)行隊(duì)列有一個(gè)Work Stealing 機(jī)制,也就是當(dāng)某一個(gè)本地待執(zhí)行隊(duì)列未空時(shí)會(huì)從其他本地待執(zhí)行隊(duì)列中取一半過來用。
本地待執(zhí)行隊(duì)列是一個(gè)環(huán)形隊(duì)列,并且有數(shù)量限制(通常是256)
全局待執(zhí)行隊(duì)列是一個(gè)鏈表(同常規(guī)鏈表一樣,有一個(gè)head、tail指針),保存在全局變量sched中,入隊(duì)和出隊(duì)存在鎖操作。
空閑machine鏈表:
沒有任務(wù)執(zhí)行的machine保存在一個(gè)鏈表中,這個(gè)空閑鏈表也是存放在全局變量sched中的,空閑的machine(休眠態(tài))等待一個(gè)信號量(m.park), 喚醒時(shí)會(huì)使用這個(gè)信號量。
所以說machine大致分為這么三類:正在執(zhí)行的machine、在空閑鏈表中的machine、自旋狀態(tài)的machine
為了保證有足夠的machine來執(zhí)行g(shù)oroutine:
1、有新的goroutine, 如果當(dāng)前無自旋的machine但是有空閑的process, 就喚醒或者新建一個(gè)machine。
2、當(dāng)machine準(zhǔn)備運(yùn)行出隊(duì)的goroutine時(shí), 如果當(dāng)前無自旋的machine,但是有空閑的process, 就喚醒或者新建一個(gè)machine
3、當(dāng)machine離開自旋狀態(tài)并準(zhǔn)備休眠時(shí), 會(huì)在離開自旋狀態(tài)后再次檢查所有運(yùn)行隊(duì)列, 如果有待運(yùn)行的goroutine則重新進(jìn)入自旋狀態(tài),并進(jìn)入競爭態(tài)。

因?yàn)?入隊(duì)待運(yùn)行的G"和"M離開自旋狀態(tài)"會(huì)同時(shí)進(jìn)行, 為避免待運(yùn)行的goroutine入隊(duì)了, 也有空閑的process, 但無machine去執(zhí)行的情況,go會(huì)這樣去檢查:
1、入隊(duì)待運(yùn)行的goroutine
內(nèi)存屏障
2、檢查當(dāng)前自旋的machine數(shù)量
3、喚醒或者新建一個(gè)machine
4、減少當(dāng)前自旋的M數(shù)量
內(nèi)存屏障
5、檢查所有運(yùn)行隊(duì)列是否有待運(yùn)行的goroutine
6、休眠
空閑process鏈表:
空閑process鏈表用于保存空閑的process,保存在全局變量sched中。
上面提到的一個(gè)sched本質(zhì)上就是一個(gè)調(diào)度器,定義在proc.c中。
關(guān)于更多的實(shí)現(xiàn)細(xì)節(jié),還有更加詳盡的源碼剖析和底層匯編發(fā)生了什么可以參考一下這篇blog(文章寫的非常好,力推):
https://studygolang.com/articles/11627
上面提到的work stealing 實(shí)際上是一個(gè)調(diào)度算法,詳細(xì)的大家可以看一下這篇論文。
http://supertech.csail.mit.edu/papers/steal.pdf
關(guān)于go協(xié)程的介紹,本篇暫時(shí)就先介紹這么多。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容