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操作導致阻塞執行緒。
以上內容純屬原創,如有問題歡迎指正!