詳解GO語言Goroutine與線程的區別

我們在使用Go語言進行開發時,一般會使用goroutine來處理併發任務。那麼大家有沒有考慮過goroutine的實現機制是什麼樣的?很多同學會把goroutine與線程等同起來,但是實際上並不是這樣的。在這邊文章中,我們將介紹以下內容:

  • 什麼是goroutine?
  • Goroutine與線程的區別
  • Goroutine是如何調度的
詳解GO語言Goroutine與線程的區別

什麼是goroutine?

Goroutine是建立在線程之上的輕量級的抽象。它允許我們以非常低的代價在同一個地址空間中並行地執行多個函數或者方法。相比於線程,它的創建和銷燬的代價要小很多,並且它的調度是獨立於線程的。在golang中創建一個goroutine非常簡單,使用“go”關鍵字即可:

package main

import ( 
 "fmt"
 "time"
)

func learning() { 
 fmt.Println("My first goroutine")
}
func main() { 
 go learning()
 /* we are using time sleep so that the main program does not terminate before the execution of goroutine.*/
 time.Sleep(1 * time.Second)
 fmt.Println("main function")
}

這段代碼的輸出是這樣的:

My first goroutine

main function

如果把Sleep去掉的話,輸出就會變成:

main function

這是因為,和線程一樣,golang的主函數(其實也跑在一個goroutine中)並不會等待其它goroutine結束。如果主goroutine結束了,所有其它goroutine都將結束。

Goroutine與線程的區別

許多人認為goroutine比線程運行得更快,這是一個誤解。Goroutine並不會更快,它只是增加了更多的併發性。當一個goroutine被阻塞(比如等待IO),golang的scheduler會調度其它可以執行的goroutine運行。與線程相比,它有以下幾個優點:

內存消耗更少:

Goroutine所需要的內存通常只有2kb,而線程則需要1Mb(500倍)。

創建與銷燬的開銷更小

由於線程創建時需要向操作系統申請資源,並且在銷燬時將資源歸還,因此它的創建和銷燬的開銷比較大。相比之下,goroutine的創建和銷燬是由go語言在運行時自己管理的,因此開銷更低。

詳解GO語言Goroutine與線程的區別

切換開銷更小

這是goroutine於線程的主要區別,也是golang能夠實現高併發的主要原因。線程的調度方式是搶佔式的,如果一個線程的執行時間超過了分配給它的時間片,就會被其它可執行的線程搶佔。在線程切換的過程中需要保存/恢復所有的寄存器信息,比如16個通用寄存器,PC(Program Counter),SP(Stack Pointer),段寄存器等等。

而goroutine的調度是協同式的,它不會直接地與操作系統內核打交道。當goroutine進行切換的時候,之後很少量的寄存器需要保存和恢復(PC和SP)。因此gouroutine的切換效率更高。

Goroutine的調度

真如前面提到的,goroutine的調度方式是協同式的。在協同式調度中,沒有時間片的概念。為了並行執行goroutine,調度器會在以下幾個時間點對其進行切換:

  • Channel接受或者發送會造成阻塞的消息
  • 當一個新的goroutine被創建時
  • 可以造成阻塞的系統調用,如文件和網絡操作
  • 垃圾回收

下面讓我們來看一下調度器具體是如何工作的。Golang調度器中有三個概念

  • Processor(P)
  • OSThread(M)
  • Goroutines(G)

在一個Go程序中,可用的線程數是通過GOMAXPROCS來設置的,默認值是可用的CPU核數。我們可以用runtime包動態改變這個值。OSThread調度在processor上,goroutines調度在OSThreads上,如下圖所示

詳解GO語言Goroutine與線程的區別

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。

詳解GO語言Goroutine與線程的區別

Search in the local queue if not found

在每一輪調度中,scheduler找到一個可以運行的goroutine並執行直到其被阻塞。

Search in the local queue
 if not found 
 Try to steal from other Ps' local queue //see Fig 3
 if not found 
 Search in the global queue 
 Also periodically it searches in the global queue (every ~ 1/70)

由此可見,操作系統的一個線程下可以併發執行上千個goroutine,每個goroutine所佔用的資源和切換開銷都很小,因此,goroutine是golang適合高併發場景的重要原因。


分享到:


相關文章: