我們在使用Go語言進行開發時,一般會使用goroutine來處理併發任務。那麼大家有沒有考慮過goroutine的實現機制是什麼樣的?很多同學會把goroutine與執行緒等同起來,但是實際上並不是這樣的。在這邊文章中,我們將介紹以下內容:
什麼是goroutine?
Goroutine與執行緒的區別
Goroutine是如何排程的
什麼是goroutine?
Goroutine是建立線上程之上的輕量級的抽象。它允許我們以非常低的代價在同一個地址空間中並行地執行多個函式或者方法。相比於執行緒,它的建立和銷燬的代價要小很多,並且它的排程是獨立於執行緒的。在golang中建立一個goroutine非常簡單,使用“go”關鍵字即可:
package mainimport ( “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語言在執行時自己管理的,因此開銷更低。
切換開銷更小
這是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上,如下圖所示
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。
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適合高併發場景的重要原因。