Go 分布式学习利器(17)-- Go并发编程之协程机制:Grountine 原理及使用
文章目錄
- 1. Thread VS Groutine
- 2. Groutine 調度原理
- 3. Groutine 示例代碼
關于Go的底層實現還需要后續持續研究,文中如有一些原理描述有誤,歡迎指證。
1. Thread VS Groutine
這里主要介紹一下Go的并發協程相比于傳統的線程 的不同點:
-
創建時默認的stack大小
JDK5 以后Java thread stack默認大小為1M
C++ 的thread stack 默認大小為8M
Grountine 的 Stack初始化大小為2K所以Grountine 大批量創建的時候速度會更快
-
和 KSE(Kernel Space Entity即內核線程)的對應關系
Java Thread是1:1
Groutine 是M:N,多對多,如下圖。
內核線程是由CPU直接調度,如果一個用戶線程對應一個內核線程,調度效率來看肯定是快于多個用戶線程對應一個內核線程的。然而,實際的開發環境中一個用戶線程對應一個內核線程 在 高并發場景下出現的頻繁內核線程上下文切換(保留線程上下文,更新CPU內部各種寄存器)對系統性能的影響占主要部分。而Go語言內部實現的線程調度器提供了多個用戶線程和一個內核線程對應,這樣在高并發場景能夠有效降低線程間切換帶來的性能消耗。
當然,如果如果僅僅只有幾個或者十幾個(小于CPU核數)用戶線程的應用可能就體現不出Grountine的優勢了。
2. Groutine 調度原理
如下圖:
- 編號1: 沒有被用戶線程使用的內核線程
- 編號2: Go自實現的協程調度器P 對應一個內核線程M0。在該調度器中維護了一個協程隊列,一個時刻可以有一個協程正在運行G0,其他的協程在調度隊列中等待被執行。
- 編號3: 協程處理器沒有正在執行的協程,可以從調度隊列中取出一個協程
- 編號4: 協程可以通過系統調用來和內核線程綁定
問題1:如果一個正在運行的G協程將Processor 獨占時間較久,導致后續排隊的協程一直無法運行,Processor如何處理?
Go 運行之后,后臺會維護一個守護線程來進行計數 ,表示一個Processor 完成的協程數量。當一段時間發現這個計數沒有更新,會向當前正在執行的協程任務棧插入一個flag,協程運行時遇到非inline函數時會讀取到這個標記,會將正在運行的自己中斷并插入到等待Processor調度的隊尾,Processor此時會繼續調度其他的協程進行運行。
問題2: 當一個協程(內核線程的執行)被系統中斷(CPU需要調度I/O線程),Processor會有什么樣的行為?
Processor并不會就此等待中斷處理完成,而是為了保證并發,將自己和另一個內核線程綁定,繼續執行調度隊列中的等待被執行的協程。當中斷的協程被喚醒,則會將自己加入到另一個Processor的等待隊列或者全局等待隊列中。
當一個協程被中斷,它在CPU寄存器中的狀態會被保存在自己的協程對象中,重新獲得執行機會時這一些狀態會被寫入到新的寄存器中繼續運行。
通過以上調度可以看到,Groutine能夠在不同的場景下仍然希望保持較高的并發繼續執行來保證自己的高并發性能。
3. Groutine 示例代碼
啟動一個Groutine非常簡單
go func(){}() 的方式
測試代碼如下, 注意使用方法一的值傳遞方式啟動go routine 。
package groutine_testimport ("fmt""testing""time"
)func TestGroutine(t *testing.T) {for i := 0; i < 10; i ++ {// 方法一: 正確go func(i int) { // 啟動 一個 go routinefmt.Println(i)}(i)// 方法二:錯誤// 如下代碼是有問題的// i 地址是被所有協程共享的,這個時候打印的結果// 會受到其他協程的影響// 想要保證代碼的正確性,即每一個go routine打印// 各自的i 值,需要利用如上啟動go routine的代碼,// 進行值傳遞,從而讓每個goroutine 獨享各自的i的地址。// go func() {// fmt.Println(i)// }()}time.Sleep(time.Millisecond*50)
}
總結
以上是生活随笔為你收集整理的Go 分布式学习利器(17)-- Go并发编程之协程机制:Grountine 原理及使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿Q正传高清版
- 下一篇: Go 分布式学习利器(18)-- Go并