1、什么是Goroutine?
Goroutine是建立在線程之上的輕量級的抽象。它允許我們以非常低的代價在同一個地址空間中並行地執行多個函數或者方法。相比於線程,它的創建和銷毀的代價要小很多,並且它的調度是獨立於線程的。
package main import ( "fmt" "time" ) func learning() { fmt.Println("My first goroutine") } func main() { go learning() time.Sleep(1 * time.Second) fmt.Println("main function") }
這段代碼的輸出是:
My first goroutine
main function
如果將Sleep去掉,將會輸出的是:
main function
這是因為,和線程一樣,golang的主函數(其實也是跑在一個goroutine中)並不會等待其他goroutine結束。如果主goroutine結束了,所有其他goroutine都將結束。
2、Goroutine與線程的區別
許多人認為goroutine比線程運行得更快,這是一個誤解。Goroutine並不會更快,它只是增加了更多的並發性。當一個goroutine被阻塞(比如等待IO),golang的scheduler會調度其他可以執行的goroutine運行。與線程相比,它有以下的幾個優點:
內存消耗更少:
Goroutine所需要的內存通常只有2kb,而線程則需要1Mb(500倍)
創建與銷毀的開銷更小:
由於線程創建時需要向操作系統申請資源,並且在銷毀時將資源歸還,因此它的創建和銷毀的開銷比較大。相比之下,goroutine的創建和銷毀是由go語言在運行時自己管理的,因此開銷更低。
切換開銷更小:
只是goroutine之於線程的主要區別,也是golang能夠實現高並發的主要原因。線程的調度方式是搶占式的,如果一個線程的執行時間超過了分配給它的時間片,就會被其他可執行的線程搶占。在線程切換的過程中需要保存/恢復所有的寄存器信息,比如16個通用寄存器,PC(Program Counter)、SP(Stack Pointer)段寄存器等等。而goroutine的調度是協同式的,它不會直接地與操作系統內核打交道。當goroutine進行切換的時候,之后很少量的寄存器需要保存和恢復(PC和SP)。因此goroutine的切換效率更高。
3、Goroutine的調度
goroutine的調度方式是協同式的,在協同式調度中,沒有時間片的概念。為了並行執行goroutine,調度器會在以下幾個時間點對其進行切換:
- Channel接收或者發送會造成阻塞的消息
- 當一個新的goroutine被創建時
- 可以造成阻塞的系統調用,如文件和網絡操作
- 垃圾回收
調度器具體是如何工作的呢,Golang調度器中有三個概念:
- Processor(P)
- OSThread(M)
- Goroutines(G)
在一個Go程序中,可用的線程數是通過GOMAXPROCS來設置的,默認值是可用的CPU核數。我們可以用runtime包來動態改變這個值。OSThread調度在processor上,goroutines調度在OSThreads上。
Golang的調度器可以利用多processor資源,在任意時刻,M個goroutine需要被調度到N個OS threads上,同時這些threads運行在至多GOMAXPROCS個processor上(N <= GOMAXPROCS)。Go scheduler將可運行的goroutines分配到多個運行在一個或多個processor上的OS threads上。
每個processor有一個本地goroutine隊列。同時有一個全局的goroutine隊列。每個OSThread都會被分配給一個processor。最多只能有GOMAXPROCS個processor,每個processor同時只能執行一個OSThread。Scheculer可以根據需要創建OSThread。
在每一輪調度中,scheduler找到一個可以運行的goroutine並執行直到其被阻塞。由此可見,操作系統的一個線程下可以並發執行上千個goroutine,每個goroutine所占用的資源和切換開銷都很小,因此,goroutine是golang適合高並發場景的重要原因。