goroutine上下文切換機制

夏笑聲發表於2019-07-15

  goroutine是go語言的協程,go語言在語言和編譯器層面提供對協程的支援。goroutine跟執行緒一個很大區別就是執行緒是作業系統的物件,而goroutine是應用層實現的執行緒。goroutine實際上是執行線上程池上的,由go的runtime實現排程,goroutine排程時,由於不需要像執行緒一樣涉及到系統呼叫,要進行使用者態和核心態的切換,因此,goroutine被稱為輕量級的執行緒,開銷要比執行緒小很多。然而,這裡我想到了一個問題,執行緒是由作業系統進行排程的,作業系統有對處理器的排程許可權,因此執行緒在上下文切換時,作業系統可以從正在佔用處理器的執行緒手中剝奪處理器的使用權,然而goroutine該怎麼完成這個操作呢?

  然而goroutine並不能像執行緒的排程那樣,goroutine排程時,必須由當前正在佔用CPU的goroutine主動讓出CPU給新的goroutine,才能完成切換操作。

  具體實現是這樣的,go對所有的系統呼叫進行了封裝,當前執行的goroutine如果正在執行系統呼叫或者可能會導致當前goroutine阻塞的操作時,runtime就會把當前這個goroutine切換掉。因此一個很有意思的事情就發生了,如果當前的goroutine沒有出現上述的可能會導致goroutine切換的條件時,就可以一直佔用CPU(實際上只是一直佔用執行緒),而且並不會因為這個goroutine佔用時間太長而進行切換。我們可以通過如下這段程式碼進行驗證:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6 )
 7 
 8 func process(id int) {
 9     fmt.Printf("id: %d\n", id)
10     for {
11     }
12 }
13 func main() {
14     var wg sync.WaitGroup
15     n := 10
16     wg.Add(n)
17     for i := 0; i < n; i++ {
18         go process(i)
19     }
20     wg.Wait()
21 }

這段程式碼輸出如下:

id: 9
id: 5
id: 6
id: 0

  按照正常的邏輯,這段程式碼應該會輸出0到9一共十個id,然而執行後發現,只輸出了四個(GOMAXPROCS: goroutine底層執行緒池最大執行緒數,預設為硬體執行緒數)id,這就說明實際只有四個goroutine得到了CPU,而且沒有進行切換,因為process這個方法裡面沒有會導致goroutine切換的條件。然後我們在for迴圈裡面加入一個操作,例如time.Sleep()或者make分配記憶體等等

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6     "time"
 7 )
 8 
 9 func process(id int) {
10     fmt.Printf("id: %d\n", id)
11     for {
12         time.Sleep(time.Second)
13     }
14 }
15 func main() {
16     var wg sync.WaitGroup
17     n := 10
18     wg.Add(n)
19     for i := 0; i < n; i++ {
20         go process(i)
21     }
22     wg.Wait()
23 }

Output:

id: 2
id: 0
id: 1
id: 9
id: 6
id: 3
id: 7
id: 8
id: 5
id: 4

  可以看到這次的輸出就是我們預料的結果了。在知道goroutine的排程策略之後,可以想到這種策略可能會帶來的問題,假如有n個goroutine出現阻塞,並且n >= GOMAXPROCS時,將會導致整個程式阻塞。

  然而這個問題是無法從根本上解決的,所以go給我們提供了一個方法runtime.Gosched(),呼叫這個方法可以讓當前的goroutine主動讓出CPU,這也不失為一個彌補的好方法了。而且在對go程式效能調優的時候,我們可以根據實際情況來調整GOMAXPROCS的值,例如當有密集的IO操作時,儘量把這個值設定大一點,可以避免由於大量IO操作導致阻塞執行緒。

 

以上內容純屬原創,如有問題歡迎指正!

相關文章