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

學無先后,達者為師

網站首頁 編程語言 正文

Golang并發編程深入分析_Golang

作者:Mingvvv ? 更新時間: 2022-12-15 編程語言

Go 協程和普通線程對比

Go 擁有極強的并發編程能力,而 Go 并發編程強勢原因,一部分原因是因為語法簡單 ,還有一個更核心的原因是 Go 中協程 goroutine (用戶態線程)的存在。

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

內核級線程(線程)

內核態線程簡稱線程,是在內核中維護了線程表進行跟蹤監控的線程。是 CPU 調度和分派的基本單位,是具備進程某些屬性,能夠獨立運行的更小單位,所以也稱為輕量級進程。

如果使用內核態線程在一個 CPU 上實現任務的并發,那么 CPU 會通過分配時間片的方式去執行任務,當正在運行的任務所分配的時間片用完了,就會切換到另一個任務,而在切換的之前會保存當前任務的狀態(CPU寄存器、程序計數器中的內容),當下次再次切換到這個任務之前就會先加載之前保存到任務狀態,這個過程稱作上下文切換。

而在上下文切換的過程該線程會從用戶態轉為內核態,加載時再由內核態轉為用戶態。而用戶態和內核態切換的代價是很高的,從用戶態到內核態的切換比較耗費資源的,而且內核資源是很珍貴的,所以內核態線程過多時,可能會出現內核資源耗盡的情況。

線程優點

  • 多核 CPU 利用 :內核具有0級權限,因此可以在多個 CPU 上執行內核線程
  • 操作系統級優化:線程之間是相對獨立的,一個線程阻塞不會影響其他線程的執行。內核態線程在進行你 IO 操作時不需要進行系統調用 。

線程缺點

  • 創建的時候需要切換到內核態,所以創建成本比較高。
  • 切換的時候,需要用戶態和內核態之間頻繁的切換,切換成本較高。
  • 線程數量有限制,當內核態線程過多時,會出現內核資源耗盡的情況。

用戶級線程(協程)

用戶態線程簡稱協程,是完全由用戶控制的線程,內核并不會感知到它的存在,用戶級線程的創建、銷毀、調度、狀態變更以及其中的代碼和數據都完全需要我們的程序自己去實現和處理。因為是存在在用戶空間上的線程,所以在切換時不存在用戶態和內核態的轉換。

協程優點

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

協程缺點

  • 因為操作系統的內核看不到協程,所以同屬于一個進程的協程只能占有一個核,不能發揮多核優勢。
  • 操作系統不能主動調度協程,所以后面的協程只能等待前面的寫成執行完才能獲取到 CPU 資源。
  • 同上,當前協程阻塞會導致后面的寫成無法執行。內核協作成本高,當要進行一些高權限的操作,比如讀寫文件時,需要頻繁地進行用戶態和內核態的切換。

調度器(GPM)

GPM 是 Go 語言運行時系統的重要組成部分

其中 G 代表 goroutine 即協程,M 代表 machine 即系統線程、P 代表 Proccessor 代表寫成和線程之間的聯系

簡單點理解就是:

G 是要被執行的任務實例

M 是實際的執行載體,需要綁定 P 成為一個執行單元才能調度 G

P 可以看作是任務處理器,P 中保存 M 執行 G 時的一些資源,P 決定了哪個 G 能配分配到哪個 M 上

G\P\M 之間的工作關系

G 要調度到 M 上才能運行,M 需要關聯 P 才可以執行 Go 代碼,但當處理阻塞或系統調用中時,M 可以不用關聯 P

當進程啟動時,會創建若干個(一般是內核數量)系統線程 M 和任務處理器 P,并一一綁定,因為一個線程對應一個 CPU 就不會出現上下文的切切換,能更大限度的節省資源。同時每一個任務處理器 P 都會對應一個私有的任務隊列,但是私有隊列有上限的,所以所有任務處理器還會有一個公用的全局隊列。

當有一個 G 被創建時,就會被放到一個任務處理器 P 的私有隊列中等待被調度。如果所有的私有隊列都滿了,就會被放到全局隊列中去。當 P 尋找 G 的時候,會先從自己的私有隊列中尋找,如果沒找到,再去全局隊列中尋找,如果還是沒有,它會去搶別的 P 中的 G。當任何地方都不能找到需要調度的 G 時,M 和 P 則會斷開連接。

當 G 中進行了系統調用,則 M 也會進入系統調用狀態,此時 P 會去尋找其他未在工作的 M 并為其尋找需要調度的任務。當 G 完成了系統調用,會進入空閑的私有隊列或者全局隊列,然后等待再次被調度。

所以,G、M 之間沒有直接的聯系,一個 G 可能被不同的 M 調度,一個 M 也可以調度不同的 G。

Go 使用協程

創建協程

通過關鍵字 go 創建一個協程

一:普通函數創建

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

二:匿名函數創建

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

三:匿名帶參數函數創建

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)
}

注意

一:主協程不會等待子協程執行完畢

以下列代碼為例,一般來說控制臺不會打印任何東西。原因是當代碼運行到 go goroutineTest(i) 這一行時,并不是真正的會運行它,而是將它放到了任務隊列中。所以可能子協程還沒開始執行,主協程就已經退出了。所以我們可以再主協程加上等待。

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

二:協程的執行沒有順序

同上,當寫成創建是,并不是立即執行,而是被分到不同的隊列中等待被調度,而調度是不能保證先后順序的(只能保證同一個隊列中的任務先進先出),因此上述代碼打印出來的結果大部分情況下時亂序的。

三:無參匿名函數中的變量變化

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

這段代碼的打印結果如下:

2
2
3

因為匿名函數不攜帶參數信息,只有匿名方法執行到 fmt.Println(i) 這一行時才會去讀取 i 的值。又因為協程執行是有延遲的,所以當執行到這一句時, i 的值可能已經發生了變化。

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

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

欄目分類
最近更新