Goroutine是如何工作的?

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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

Go語言

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

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

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。切換的開銷就小多了。

前面已經談到了,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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.