Golang并發(fā)編程深入分析
Go 協(xié)程和普通線程對比
Go 擁有極強的并發(fā)編程能力,而 Go 并發(fā)編程強勢原因,一部分原因是因為語法簡單 ,還有一個更核心的原因是 Go 中協(xié)程 goroutine (用戶態(tài)線程)的存在。
在 Go 中 goroutine 是比普通線程(內(nèi)核態(tài)線程)更加輕量化的存在。
內(nèi)核級線程(線程)
內(nèi)核態(tài)線程簡稱線程,是在內(nèi)核中維護(hù)了線程表進(jìn)行跟蹤監(jiān)控的線程。是 CPU 調(diào)度和分派的基本單位,是具備進(jìn)程某些屬性,能夠獨立運行的更小單位,所以也稱為輕量級進(jìn)程。
如果使用內(nèi)核態(tài)線程在一個 CPU 上實現(xiàn)任務(wù)的并發(fā),那么 CPU 會通過分配時間片的方式去執(zhí)行任務(wù),當(dāng)正在運行的任務(wù)所分配的時間片用完了,就會切換到另一個任務(wù),而在切換的之前會保存當(dāng)前任務(wù)的狀態(tài)(CPU寄存器、程序計數(shù)器中的內(nèi)容),當(dāng)下次再次切換到這個任務(wù)之前就會先加載之前保存到任務(wù)狀態(tài),這個過程稱作上下文切換。
而在上下文切換的過程該線程會從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài),加載時再由內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)。而用戶態(tài)和內(nèi)核態(tài)切換的代價是很高的,從用戶態(tài)到內(nèi)核態(tài)的切換比較耗費資源的,而且內(nèi)核資源是很珍貴的,所以內(nèi)核態(tài)線程過多時,可能會出現(xiàn)內(nèi)核資源耗盡的情況。
線程優(yōu)點
- 多核 CPU 利用 :內(nèi)核具有0級權(quán)限,因此可以在多個 CPU 上執(zhí)行內(nèi)核線程
- 操作系統(tǒng)級優(yōu)化:線程之間是相對獨立的,一個線程阻塞不會影響其他線程的執(zhí)行。內(nèi)核態(tài)線程在進(jìn)行你 IO 操作時不需要進(jìn)行系統(tǒng)調(diào)用 。
線程缺點
- 創(chuàng)建的時候需要切換到內(nèi)核態(tài),所以創(chuàng)建成本比較高。
- 切換的時候,需要用戶態(tài)和內(nèi)核態(tài)之間頻繁的切換,切換成本較高。
- 線程數(shù)量有限制,當(dāng)內(nèi)核態(tài)線程過多時,會出現(xiàn)內(nèi)核資源耗盡的情況。
用戶級線程(協(xié)程)
用戶態(tài)線程簡稱協(xié)程,是完全由用戶控制的線程,內(nèi)核并不會感知到它的存在,用戶級線程的創(chuàng)建、銷毀、調(diào)度、狀態(tài)變更以及其中的代碼和數(shù)據(jù)都完全需要我們的程序自己去實現(xiàn)和處理。因為是存在在用戶空間上的線程,所以在切換時不存在用戶態(tài)和內(nèi)核態(tài)的轉(zhuǎn)換。
協(xié)程優(yōu)點
- 不由內(nèi)核管理,由用戶自主管理,創(chuàng)建成本比較低。
- 不存在內(nèi)核態(tài)和用戶態(tài)的切換,切換成本比較低。
- 不需要操作系統(tǒng)去調(diào)度,所以可以跨操作系統(tǒng),并且更加靈活、更容易控制。
協(xié)程缺點
- 因為操作系統(tǒng)的內(nèi)核看不到協(xié)程,所以同屬于一個進(jìn)程的協(xié)程只能占有一個核,不能發(fā)揮多核優(yōu)勢。
- 操作系統(tǒng)不能主動調(diào)度協(xié)程,所以后面的協(xié)程只能等待前面的寫成執(zhí)行完才能獲取到 CPU 資源。
- 同上,當(dāng)前協(xié)程阻塞會導(dǎo)致后面的寫成無法執(zhí)行。內(nèi)核協(xié)作成本高,當(dāng)要進(jìn)行一些高權(quán)限的操作,比如讀寫文件時,需要頻繁地進(jìn)行用戶態(tài)和內(nèi)核態(tài)的切換。
調(diào)度器(GPM)
GPM 是 Go 語言運行時系統(tǒng)的重要組成部分
其中 G 代表 goroutine 即協(xié)程,M 代表 machine 即系統(tǒng)線程、P 代表 Proccessor 代表寫成和線程之間的聯(lián)系
簡單點理解就是:
G 是要被執(zhí)行的任務(wù)實例
M 是實際的執(zhí)行載體,需要綁定 P 成為一個執(zhí)行單元才能調(diào)度 G
P 可以看作是任務(wù)處理器,P 中保存 M 執(zhí)行 G 時的一些資源,P 決定了哪個 G 能配分配到哪個 M 上
G\P\M 之間的工作關(guān)系
G 要調(diào)度到 M 上才能運行,M 需要關(guān)聯(lián) P 才可以執(zhí)行 Go 代碼,但當(dāng)處理阻塞或系統(tǒng)調(diào)用中時,M 可以不用關(guān)聯(lián) P
當(dāng)進(jìn)程啟動時,會創(chuàng)建若干個(一般是內(nèi)核數(shù)量)系統(tǒng)線程 M 和任務(wù)處理器 P,并一一綁定,因為一個線程對應(yīng)一個 CPU 就不會出現(xiàn)上下文的切切換,能更大限度的節(jié)省資源。同時每一個任務(wù)處理器 P 都會對應(yīng)一個私有的任務(wù)隊列,但是私有隊列有上限的,所以所有任務(wù)處理器還會有一個公用的全局隊列。
當(dāng)有一個 G 被創(chuàng)建時,就會被放到一個任務(wù)處理器 P 的私有隊列中等待被調(diào)度。如果所有的私有隊列都滿了,就會被放到全局隊列中去。當(dāng) P 尋找 G 的時候,會先從自己的私有隊列中尋找,如果沒找到,再去全局隊列中尋找,如果還是沒有,它會去搶別的 P 中的 G。當(dāng)任何地方都不能找到需要調(diào)度的 G 時,M 和 P 則會斷開連接。
當(dāng) G 中進(jìn)行了系統(tǒng)調(diào)用,則 M 也會進(jìn)入系統(tǒng)調(diào)用狀態(tài),此時 P 會去尋找其他未在工作的 M 并為其尋找需要調(diào)度的任務(wù)。當(dāng) G 完成了系統(tǒng)調(diào)用,會進(jìn)入空閑的私有隊列或者全局隊列,然后等待再次被調(diào)度。
所以,G、M 之間沒有直接的聯(lián)系,一個 G 可能被不同的 M 調(diào)度,一個 M 也可以調(diào)度不同的 G。
Go 使用協(xié)程
創(chuàng)建協(xié)程
通過關(guān)鍵字 go 創(chuàng)建一個協(xié)程
一:普通函數(shù)創(chuàng)建
func goroutineTest(i int) { fmt.Println(i) } func main() { for i := 0; i < 10; i++ { go goroutineTest(i) } time.Sleep(time.Millisecond * 500) }
二:匿名函數(shù)創(chuàng)建
func main() { for i := 0; i < 10; i++ { go func() { fmt.Println(i) }() } time.Sleep(time.Millisecond * 500) }
三:匿名帶參數(shù)函數(shù)創(chuàng)建
func main() { for i := 0; i < 10; i++ { go func(str string) { fmt.Println(str) }(fmt.Sprintf("%s%d", "s", i)) } time.Sleep(time.Millisecond * 500) }
注意
一:主協(xié)程不會等待子協(xié)程執(zhí)行完畢
以下列代碼為例,一般來說控制臺不會打印任何東西。原因是當(dāng)代碼運行到 go goroutineTest(i) 這一行時,并不是真正的會運行它,而是將它放到了任務(wù)隊列中。所以可能子協(xié)程還沒開始執(zhí)行,主協(xié)程就已經(jīng)退出了。所以我們可以再主協(xié)程加上等待。
func goroutineTest(i int) { fmt.Println(i) } func main() { for i := 0; i < 10; i++ { go goroutineTest(i) } time.Sleep(time.Millisecond * 500) }
二:協(xié)程的執(zhí)行沒有順序
同上,當(dāng)寫成創(chuàng)建是,并不是立即執(zhí)行,而是被分到不同的隊列中等待被調(diào)度,而調(diào)度是不能保證先后順序的(只能保證同一個隊列中的任務(wù)先進(jìn)先出),因此上述代碼打印出來的結(jié)果大部分情況下時亂序的。
三:無參匿名函數(shù)中的變量變化
func main() { for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() } time.Sleep(time.Millisecond * 500) }
這段代碼的打印結(jié)果如下:
2
2
3
因為匿名函數(shù)不攜帶參數(shù)信息,只有匿名方法執(zhí)行到 fmt.Println(i) 這一行時才會去讀取 i 的值。又因為協(xié)程執(zhí)行是有延遲的,所以當(dāng)執(zhí)行到這一句時, i 的值可能已經(jīng)發(fā)生了變化。
下一篇再研究主協(xié)程等待子協(xié)程執(zhí)行完畢以及確保子協(xié)程執(zhí)行順序的方法…
到此這篇關(guān)于Golang并發(fā)編程深入分析的文章就介紹到這了,更多相關(guān)Golang并發(fā)編程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang學(xué)習(xí)筆記(二):類型、變量、常量
這篇文章主要介紹了Golang學(xué)習(xí)筆記(二):類型、變量、常量,本文講解了基本類型、保留字、變量、常量、枚舉、運算符、指針、分組聲明等內(nèi)容,需要的朋友可以參考下2015-05-05基于Go+OpenCV實現(xiàn)人臉識別功能的詳細(xì)示例
OpenCV是一個強大的計算機(jī)視覺庫,提供了豐富的圖像處理和計算機(jī)視覺算法,本文將向你介紹在Mac上安裝OpenCV的步驟,并演示如何使用Go的OpenCV綁定庫進(jìn)行人臉識別,需要的朋友可以參考下2023-07-07Goland使用delve進(jìn)行遠(yuǎn)程調(diào)試的詳細(xì)教程
網(wǎng)上給出的使用delve進(jìn)行遠(yuǎn)程調(diào)試,都需要先在本地交叉編譯或者在遠(yuǎn)程主機(jī)上編譯出可運行的程序,然后再用delve在遠(yuǎn)程啟動程序,本教程會將上面的步驟簡化為只需要兩步,1,在遠(yuǎn)程運行程序2,在本地啟動調(diào)試,需要的朋友可以參考下2024-08-08