Goroutine是如何工作的?

翻譯原文鏈接? ?轉帖/轉載請注明出處

英文原文鏈接 發(fā)表于2014/02/24

Go語言

如果你剛剛接觸Go語言,或者說你并不理解“并發(fā)不等于并行”這句話的含義,那么Rob Pike的講座值得一看(在youtube上)。這個視頻有30分鐘長,我保證花30分鐘看這段視頻是非常值得的。

這里摘錄一段他提到的并發(fā)和并行之間的區(qū)別:“當大家聽到并發(fā)這個詞的時候,他們往往想到的是并行。并行是一個相關,但卻完全不同的概念。當我們編程的時候,并發(fā)指的是多個獨立運行的進程,而并行是指同時運行的多個計算。并發(fā)是為了一下子處理很多東西。并行是為了同時做很多事情?!?[1] (注:這里的概念有點繞。其實本質的區(qū)別在“同時”這個詞上。并行強調的時候幾個進程同時進行。而并發(fā)指的是運行多個進程,但這些進程并不需要同時被執(zhí)行。它們可以是被調度在同一個CPU分時運行的。)

Go為我們寫并發(fā)程序提供了便利。它提供了goroutine以及它們之間通信的功能。在這里我們主要討論goroutine。

Goroutine和線程的區(qū)別

Go語言使用的是goroutine,而像Java這樣的語言大多使用線程。它們之間的區(qū)別是什么呢?讓我們從三個方面來看看它們的區(qū)別:內存占用,創(chuàng)建和銷毀,以及切換開銷。

內存占用

創(chuàng)建一個goroutine不需要太多的內存 - 大概2KB左右的??臻g。如果需要更多的棧空間,就從堆里分配額外的空間來使用。[2][3] 新創(chuàng)建的線程會占用1MB的內存空間(這大約是goroutine的500倍)。這還不包括守護頁(guard page)的空間。守護頁是用來保護線程之間的內存空間不會被相互竄改。[7]

因此一個處理很多請求的服務可以為每個請求創(chuàng)建一個goroutine。但是如果為每個請求去創(chuàng)建一個線程,那么它很快就會碰到OutOfMemoryError。這不是Java獨有的問題,任何使用操作系統(tǒng)線程作為主要并發(fā)手段的編程語言都會碰到這個問題。

創(chuàng)建和銷毀的開銷

線程需要從操作系統(tǒng)里請求資源并在用完之后釋放回去,因此創(chuàng)建和銷毀線程的開銷非常大。為了避免這些開銷,我們通常的做法是維護一個線程池。Goroutine的創(chuàng)建和銷毀是由運行環(huán)境(runtime)完成的。這些操作的開銷就比較小。Go語言不支持手工管理goroutine。

切換開銷

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

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

前面已經談到了,goroutine的數目會比線程多很多,但這并不影響切換的時間。有兩個原因:第一,只有可以運行的goroutine才會被考慮,正在阻塞的goroutine會被忽略。第二,現(xiàn)代的調度器的復雜度都是O(1)的。這意味著選擇的數目(線程或者是goroutine)不會影響切換的時間。[5]

Goroutine的運行

前面談到,運行環(huán)境負責goroutine的創(chuàng)建,調度和銷毀。運行環(huán)境被會分配一些線程,用來運行所有的goroutine。在任何一個時間點,每個線程只會運行一個goroutine。如果一個goroutine被阻塞,另外一個goroutine會來替換它在對應的線程上運行。[6]

因為goroutine的調度是協(xié)同合作式的,如果一個goroutine不停的循環(huán),其它的goroutine就沒有機會被調度運行了。在Go 1.2里,這個問題的解決辦法是在調用一個函數的時候去偶爾觸發(fā)Go的調度器。這樣一個循環(huán)里如果調用了沒有被內聯(lián)的函數,它就可以被搶占了。

Goroutine的阻塞

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

- 網絡收發(fā)

- 睡眠

- channel操作

- sync包里的一些會阻塞的基本操作

即使創(chuàng)建了成千上萬的goroutine并且大多數被阻塞了,也不會造成太多的系統(tǒng)資源浪費。因為運行環(huán)境會調度另外的goroutine來運行。

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

線程和處理器

雖然我們不能直接控制運行環(huán)境創(chuàng)建多少線程,我們可以設置程序使用的處理器核數。這是通過調用runtime.GOMAXPROCS(n)函數設置GOMAXPROCS變量來實現(xiàn)的。(注:也可以通過直接設置環(huán)境變量來控制)。增加處理器核數并不意味著程序性能的提高。這取決于程序本身的設計。你的程序需要用到多少個內核數可以用剖析(profiling)工具來找到答案。

結束語

和其它語言類似,避免多個goroutine同時訪問一個共享資源是非常重要的。goroutine之間,最好是用channel來傳輸數據。有興趣的可以讀一讀“do not communicate by sharing memory; instead, share memory by communicating”。

最后,我強烈推薦讀一下C. A. R. Hoare寫的“Communicating Sequential Processes”。他是個天才。在這篇論文(1978年發(fā)表的)里,他預測了單核處理器性能最終會遇到瓶頸,然后芯片制造商們會增加處理器的內核數。他的思想對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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Goroutine是Go里的一種輕量級線程——協(xié)程。相對線程,協(xié)程的優(yōu)勢就在于它非常輕量級,進行上下文切換的代價非...
    witchiman閱讀 5,153評論 0 9
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,899評論 0 11
  • 從三月份找實習到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,815評論 11 349
  • ———你對我還是沒變 沒變的只是冷漠
    薄情人zmj閱讀 408評論 0 0
  • 小時候,雪是那么的司空見慣 第一場雪也并未顯得多么獨一無二 三五好友乘著下雪 廝混在冰天雪地之中 感受著大自然脈搏...
    鐮鼬悟閱讀 293評論 0 0

友情鏈接更多精彩內容