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