日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無(wú)先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

Golang并發(fā)編程深入分析_Golang

作者:Mingvvv ? 更新時(shí)間: 2022-12-15 編程語(yǔ)言

Go 協(xié)程和普通線程對(duì)比

Go 擁有極強(qiáng)的并發(fā)編程能力,而 Go 并發(fā)編程強(qiáng)勢(shì)原因,一部分原因是因?yàn)檎Z(yǔ)法簡(jiǎn)單 ,還有一個(gè)更核心的原因是 Go 中協(xié)程 goroutine (用戶態(tài)線程)的存在。

在 Go 中 goroutine 是比普通線程(內(nèi)核態(tài)線程)更加輕量化的存在。

內(nèi)核級(jí)線程(線程)

內(nèi)核態(tài)線程簡(jiǎn)稱線程,是在內(nèi)核中維護(hù)了線程表進(jìn)行跟蹤監(jiān)控的線程。是 CPU 調(diào)度和分派的基本單位,是具備進(jìn)程某些屬性,能夠獨(dú)立運(yùn)行的更小單位,所以也稱為輕量級(jí)進(jìn)程。

如果使用內(nèi)核態(tài)線程在一個(gè) CPU 上實(shí)現(xiàn)任務(wù)的并發(fā),那么 CPU 會(huì)通過(guò)分配時(shí)間片的方式去執(zhí)行任務(wù),當(dāng)正在運(yùn)行的任務(wù)所分配的時(shí)間片用完了,就會(huì)切換到另一個(gè)任務(wù),而在切換的之前會(huì)保存當(dāng)前任務(wù)的狀態(tài)(CPU寄存器、程序計(jì)數(shù)器中的內(nèi)容),當(dāng)下次再次切換到這個(gè)任務(wù)之前就會(huì)先加載之前保存到任務(wù)狀態(tài),這個(gè)過(guò)程稱作上下文切換。

而在上下文切換的過(guò)程該線程會(huì)從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài),加載時(shí)再由內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)。而用戶態(tài)和內(nèi)核態(tài)切換的代價(jià)是很高的,從用戶態(tài)到內(nèi)核態(tài)的切換比較耗費(fèi)資源的,而且內(nèi)核資源是很珍貴的,所以內(nèi)核態(tài)線程過(guò)多時(shí),可能會(huì)出現(xiàn)內(nèi)核資源耗盡的情況。

線程優(yōu)點(diǎn)

  • 多核 CPU 利用 :內(nèi)核具有0級(jí)權(quán)限,因此可以在多個(gè) CPU 上執(zhí)行內(nèi)核線程
  • 操作系統(tǒng)級(jí)優(yōu)化:線程之間是相對(duì)獨(dú)立的,一個(gè)線程阻塞不會(huì)影響其他線程的執(zhí)行。內(nèi)核態(tài)線程在進(jìn)行你 IO 操作時(shí)不需要進(jìn)行系統(tǒng)調(diào)用 。

線程缺點(diǎn)

  • 創(chuàng)建的時(shí)候需要切換到內(nèi)核態(tài),所以創(chuàng)建成本比較高。
  • 切換的時(shí)候,需要用戶態(tài)和內(nèi)核態(tài)之間頻繁的切換,切換成本較高。
  • 線程數(shù)量有限制,當(dāng)內(nèi)核態(tài)線程過(guò)多時(shí),會(huì)出現(xiàn)內(nèi)核資源耗盡的情況。

用戶級(jí)線程(協(xié)程)

用戶態(tài)線程簡(jiǎn)稱協(xié)程,是完全由用戶控制的線程,內(nèi)核并不會(huì)感知到它的存在,用戶級(jí)線程的創(chuàng)建、銷毀、調(diào)度、狀態(tài)變更以及其中的代碼和數(shù)據(jù)都完全需要我們的程序自己去實(shí)現(xiàn)和處理。因?yàn)槭谴嬖谠谟脩艨臻g上的線程,所以在切換時(shí)不存在用戶態(tài)和內(nèi)核態(tài)的轉(zhuǎn)換。

協(xié)程優(yōu)點(diǎn)

  • 不由內(nèi)核管理,由用戶自主管理,創(chuàng)建成本比較低。
  • 不存在內(nèi)核態(tài)和用戶態(tài)的切換,切換成本比較低。
  • 不需要操作系統(tǒng)去調(diào)度,所以可以跨操作系統(tǒng),并且更加靈活、更容易控制。

協(xié)程缺點(diǎn)

  • 因?yàn)椴僮飨到y(tǒng)的內(nèi)核看不到協(xié)程,所以同屬于一個(gè)進(jìn)程的協(xié)程只能占有一個(gè)核,不能發(fā)揮多核優(yōu)勢(shì)。
  • 操作系統(tǒng)不能主動(dòng)調(diào)度協(xié)程,所以后面的協(xié)程只能等待前面的寫成執(zhí)行完才能獲取到 CPU 資源。
  • 同上,當(dāng)前協(xié)程阻塞會(huì)導(dǎo)致后面的寫成無(wú)法執(zhí)行。內(nèi)核協(xié)作成本高,當(dāng)要進(jìn)行一些高權(quán)限的操作,比如讀寫文件時(shí),需要頻繁地進(jìn)行用戶態(tài)和內(nèi)核態(tài)的切換。

調(diào)度器(GPM)

GPM 是 Go 語(yǔ)言運(yùn)行時(shí)系統(tǒng)的重要組成部分

其中 G 代表 goroutine 即協(xié)程,M 代表 machine 即系統(tǒng)線程、P 代表 Proccessor 代表寫成和線程之間的聯(lián)系

簡(jiǎn)單點(diǎn)理解就是:

G 是要被執(zhí)行的任務(wù)實(shí)例

M 是實(shí)際的執(zhí)行載體,需要綁定 P 成為一個(gè)執(zhí)行單元才能調(diào)度 G

P 可以看作是任務(wù)處理器,P 中保存 M 執(zhí)行 G 時(shí)的一些資源,P 決定了哪個(gè) G 能配分配到哪個(gè) M 上

G\P\M 之間的工作關(guān)系

G 要調(diào)度到 M 上才能運(yùn)行,M 需要關(guān)聯(lián) P 才可以執(zhí)行 Go 代碼,但當(dāng)處理阻塞或系統(tǒng)調(diào)用中時(shí),M 可以不用關(guān)聯(lián) P

當(dāng)進(jìn)程啟動(dòng)時(shí),會(huì)創(chuàng)建若干個(gè)(一般是內(nèi)核數(shù)量)系統(tǒng)線程 M 和任務(wù)處理器 P,并一一綁定,因?yàn)橐粋€(gè)線程對(duì)應(yīng)一個(gè) CPU 就不會(huì)出現(xiàn)上下文的切切換,能更大限度的節(jié)省資源。同時(shí)每一個(gè)任務(wù)處理器 P 都會(huì)對(duì)應(yīng)一個(gè)私有的任務(wù)隊(duì)列,但是私有隊(duì)列有上限的,所以所有任務(wù)處理器還會(huì)有一個(gè)公用的全局隊(duì)列。

當(dāng)有一個(gè) G 被創(chuàng)建時(shí),就會(huì)被放到一個(gè)任務(wù)處理器 P 的私有隊(duì)列中等待被調(diào)度。如果所有的私有隊(duì)列都滿了,就會(huì)被放到全局隊(duì)列中去。當(dāng) P 尋找 G 的時(shí)候,會(huì)先從自己的私有隊(duì)列中尋找,如果沒(méi)找到,再去全局隊(duì)列中尋找,如果還是沒(méi)有,它會(huì)去搶別的 P 中的 G。當(dāng)任何地方都不能找到需要調(diào)度的 G 時(shí),M 和 P 則會(huì)斷開(kāi)連接。

當(dāng) G 中進(jìn)行了系統(tǒng)調(diào)用,則 M 也會(huì)進(jìn)入系統(tǒng)調(diào)用狀態(tài),此時(shí) P 會(huì)去尋找其他未在工作的 M 并為其尋找需要調(diào)度的任務(wù)。當(dāng) G 完成了系統(tǒng)調(diào)用,會(huì)進(jìn)入空閑的私有隊(duì)列或者全局隊(duì)列,然后等待再次被調(diào)度。

所以,G、M 之間沒(méi)有直接的聯(lián)系,一個(gè) G 可能被不同的 M 調(diào)度,一個(gè) M 也可以調(diào)度不同的 G。

Go 使用協(xié)程

創(chuàng)建協(xié)程

通過(guò)關(guān)鍵字 go 創(chuàng)建一個(gè)協(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é)程不會(huì)等待子協(xié)程執(zhí)行完畢

以下列代碼為例,一般來(lái)說(shuō)控制臺(tái)不會(huì)打印任何東西。原因是當(dāng)代碼運(yùn)行到 go goroutineTest(i) 這一行時(shí),并不是真正的會(huì)運(yùn)行它,而是將它放到了任務(wù)隊(duì)列中。所以可能子協(xié)程還沒(méi)開(kāi)始執(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í)行沒(méi)有順序

同上,當(dāng)寫成創(chuàng)建是,并不是立即執(zhí)行,而是被分到不同的隊(duì)列中等待被調(diào)度,而調(diào)度是不能保證先后順序的(只能保證同一個(gè)隊(duì)列中的任務(wù)先進(jìn)先出),因此上述代碼打印出來(lái)的結(jié)果大部分情況下時(shí)亂序的。

三:無(wú)參匿名函數(shù)中的變量變化

func main() {
	for i := 0; i < 3; i++ {
		go func() {
			 fmt.Println(i)
		}()
	}
	time.Sleep(time.Millisecond * 500)
}

這段代碼的打印結(jié)果如下:

2
2
3

因?yàn)槟涿瘮?shù)不攜帶參數(shù)信息,只有匿名方法執(zhí)行到 fmt.Println(i) 這一行時(shí)才會(huì)去讀取 i 的值。又因?yàn)閰f(xié)程執(zhí)行是有延遲的,所以當(dāng)執(zhí)行到這一句時(shí), i 的值可能已經(jīng)發(fā)生了變化。

下一篇再研究主協(xié)程等待子協(xié)程執(zhí)行完畢以及確保子協(xié)程執(zhí)行順序的方法…

原文鏈接:https://blog.csdn.net/qq_40096897/article/details/127536472

欄目分類
最近更新