Goroutine是如何工做的?

翻譯原文連接 轉帖/轉載請註明出處
英文原文連接 發表於2014/02/24
html

Go語言

若是你剛剛接觸Go語言,或者說你並不理解「併發不等於並行」這句話的含義,那麼Rob Pike的講座值得一看(在youtube上)。這個視頻有30分鐘長,我保證花30分鐘看這段視頻是很是值得的。程序員

這裏摘錄一段他提到的併發和並行之間的區別:「當你們聽到併發這個詞的時候,他們每每想到的是並行。並行是一個相關,但卻徹底不一樣的概念。當咱們編程的時候,併發指的是多個獨立運行的進程,而並行是指同時運行的多個計算。併發是爲了一會兒處理不少東西。並行是爲了同時作不少事情。」 [1] (注:這裏的概念有點繞。其實本質的區別在「同時」這個詞上。並行強調的時候幾個進程同時進行。而併發指的是運行多個進程,但這些進程並不須要同時被執行。它們能夠是被調度在同一個CPU分時運行的。)golang

Go爲咱們寫併發程序提供了便利。它提供了goroutine以及它們之間通訊的功能。在這裏咱們主要討論goroutine。編程

Goroutine和線程的區別

Go語言使用的是goroutine,而像Java這樣的語言大多使用線程。它們之間的區別是什麼呢?讓咱們從三個方面來看看它們的區別:內存佔用,建立和銷燬,以及切換開銷。網絡

內存佔用

建立一個goroutine不須要太多的內存 - 大概2KB左右的棧空間。若是須要更多的棧空間,就從堆裏分配額外的空間來使用。2 新建立的線程會佔用1MB的內存空間(這大約是goroutine的500倍)。這還不包括守護頁(guard page)的空間。守護頁是用來保護線程之間的內存空間不會被相互竄改。[7]併發

所以一個處理不少請求的服務能夠爲每一個請求建立一個goroutine。可是若是爲每一個請求去建立一個線程,那麼它很快就會碰到OutOfMemoryError。這不是Java獨有的問題,任何使用操做系統線程做爲主要併發手段的編程語言都會碰到這個問題。編程語言

建立和銷燬的開銷

線程須要從操做系統裏請求資源並在用完以後釋放回去,所以建立和銷燬線程的開銷很是大。爲了不這些開銷,咱們一般的作法是維護一個線程池。Goroutine的建立和銷燬是由運行環境(runtime)完成的。這些操做的開銷就比較小。Go語言不支持手工管理goroutine。函數

切換開銷

當一個線程阻塞的時候,另一個線程須要被調度到當前處理器上運行。線程的調度是搶佔式的(preemptively)。當切換一個線程的時候,調度器須要保存/恢復全部的寄存器。這包括16個通用寄存器,程序指針(program counter),棧指針(stack pointer),段寄存器(segment registers)和16個XMM寄存器,浮點協處理器狀態,16個AVX寄存器,全部的特殊模塊寄存器(MSR)等。當在線程間快速切換的時候這些開銷就變得很是大了。工具

Goroutine的調度是協同合做式的(cooperatively)。當切換goroutine的時候,調度器只須要保存和恢復三個寄存器 - 程序指針,棧指針和DX。切換的開銷就小多了。oop

前面已經談到了,goroutine的數目會比線程多不少,但這並不影響切換的時間。有兩個緣由:第一,只有能夠運行的goroutine纔會被考慮,正在阻塞的goroutine會被忽略。第二,現代的調度器的複雜度都是O(1)的。這意味着選擇的數目(線程或者是goroutine)不會影響切換的時間。[5]

Goroutine的運行

前面談到,運行環境負責goroutine的建立,調度和銷燬。運行環境被會分配一些線程,用來運行全部的goroutine。在任何一個時間點,每一個線程只會運行一個goroutine。若是一個goroutine被阻塞,另一個goroutine會來替換它在對應的線程上運行。[6]

由於goroutine的調度是協同合做式的,若是一個goroutine不停的循環,其它的goroutine就沒有機會被調度運行了。在Go 1.2裏,這個問題的解決辦法是在調用一個函數的時候去偶爾觸發Go的調度器。這樣一個循環裏若是調用了沒有被內聯的函數,它就能夠被搶佔了。

Goroutine的阻塞

Goroutine是廉價的,在下面這些阻塞狀況下它們也不會形成運行的線程被阻塞:

  • 網絡收發

  • 睡眠

  • channel操做

  • sync包裏的一些會阻塞的基本操做

即便建立了成千上萬的goroutine而且大多數被阻塞了,也不會形成太多的系統資源浪費。由於運行環境會調度另外的goroutine來運行。

簡而言之,goroutine是對線程的輕量化抽象。Go語言的程序員不須要直接操做線程。與此同時操做系統也不知道goroutine的存在。從操做系統的角度來看,一個Go程序有點像一個事件驅動的C程序。[5]

線程和處理器

雖然咱們不能直接控制運行環境建立多少線程,咱們能夠設置程序使用的處理器核數。這是經過調用runtime.GOMAXPROCS(n)函數設置GOMAXPROCS變量來實現的。(注:也能夠經過直接設置環境變量來控制)。增長處理器核數並不意味着程序性能的提升。這取決於程序自己的設計。你的程序須要用到多少個內核數能夠用剖析(profiling)工具來找到答案。

結束語

和其它語言相似,避免多個goroutine同時訪問一個共享資源是很是重要的。goroutine之間,最好是用channel來傳輸數據。有興趣的能夠讀一讀「do not communicate by sharing memory; instead, share memory by communicating」。

最後,我強烈推薦讀一下C. A. R. Hoare寫的「Communicating Sequential Processes」。他是個天才。在這篇論文(1978年發表的)裏,他預測了單核處理器性能最終會遇到瓶頸,而後芯片製造商們會增長處理器的內核數。他的思想對Go語言的設計影響深遠。

參考文獻

    1. Concurrency is not parallelism by Rob Pike

    2. Effective Go: Goroutines

    3. Goroutine stack size was decreased from 8kB to 2kB in Go 1.4

    4. Goroutine stacks became contiguous in Go 1.3

    5. Scheduling of goroutines by Dmitry Vyukov

    6. Analysis of the Go runtime scheduler

    7. 5 things that make Go fast by Dave Cheney

相關文章
相關標籤/搜索