Go語言並發機制初探

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

Go 語言相比Java等一個很大的優勢就是可以方便地編寫並發程式。Go 語言內建了 goroutine 機制,使用goroutine可以快速地開發並發程式, 更好的利用多核處理器資源。這篇文章學習 goroutine 的應用及其調度實現。

一、Go語言對並發的支援

使用goroutine編程

使用 go 關鍵字用來建立 goroutine 。將go聲明放到一個需調用的函數之前,在相同地址空間調用運行這個函數,這樣該函數執行時便會作為一個獨立的並發線程。這種線程在Go語言中稱作goroutine。

goroutine的用法如下:

//go 關鍵字放在方法調用前建立一個 goroutine 並執行方法體go GetThingDone(param1, param2);//建立一個匿名方法並執行go func(param1, param2) {}(val1, val2)//直接建立一個 goroutine 並在 goroutine 中執行代碼塊go {    //do someting...}

因為 goroutine 在多核 cpu 環境下是並行的。如果代碼塊在多個 goroutine 中執行,我們就實現了代碼並行。

如果我們需要瞭解程式的執行情況,怎麼拿到並行的結果呢?需要配合使用channel進行。

使用Channel控制並發

Channels用來同步並發執行的函數並提供它們某種傳值交流的機制。

通過channel傳遞的元素類型、容器(或緩衝區)和傳遞的方向由“<-”操作符指定。

可以使用內建函數 make分配一個channel:

i := make(chan int)       // by default the capacity is 0s := make(chan string, 3) // non-zero capacityr := make(<-chan bool)          // can only read fromw := make(chan<- []os.FileInfo) // can only write to

配置runtime.GOMAXPROCS

使用下面的代碼可以顯式的設定是否使用多核來執行並發任務:

runtime.GOMAXPROCS()

GOMAXPROCS的數目根據任務量分配就可以,但是不要大於cpu核心數。
配置並存執行比較適合適合於CPU密集型、並行度比較高的情景,如果是IO密集型使用多核的化會增加cpu切換帶來的效能損失。

瞭解了Go語言的並發機制,接下來看一下goroutine 機制的具體實現。

二、區別並行與並發

進程、線程與處理器

在現代作業系統中,線程是處理器調度和分配的基本單位,進程則作為資源擁有的基本單位。
每個進程是由私人的虛擬位址空間、代碼、資料和其它各種系統資源群組成。線程是進程內部的一個執行單元。
每一個進程至少有一個主執行線程,它無需由使用者去主動建立,是由系統自動建立的。
使用者根據需要在應用程式中建立其它線程,多個線程並發地運行於同一個進程中。

並行與並發

並行與並發(Concurrency and Parallelism)是兩個不同的概念,理解它們對於理解多執行緒模式非常重要。

在描述程式的並發或者並行時,應該說明從進程或者線程的角度出發。

  • 並發:一個時間段內有很多的線程或進程在執行,但何時間點上都只有一個在執行,多個線程或進程爭搶時間片輪流執行

  • 並行:一個時間段和時間點上都有多個線程或進程在執行

非並發的程式只有一個垂直的控制邏輯,在任何時刻,程式只會處在這個控制邏輯的某個位置,也就是順序執行。如果一個程式在某一時刻被多個CPU流水線同時進行處理,那麼我們就說這個程式是以並行的形式在運行。

並行需要硬體支援,單核處理器只能是並發,多核處理器才能做到並存執行。

  • 並發是並行的必要條件,如果一個程式本身就不是並發的,也就是只有一個邏輯執行順序,那麼我們不可能讓其被平行處理。

  • 並發不是並行的充分條件,一個並發的程式,如果只被一個CPU進行處理(通過分時),那麼它就不是並行的。

舉一個例子,編寫一個最簡單的程式輸出"Hello World",它就是非並發的,如果在程式中增加多線程,每個線程列印一個"Hello World",那麼這個程式就是並發的。運行時只給這個程式分配單個CPU,這個並發程式還不是並行的,需要用多核處理器的作業系統來運行它,才能實現程式的並行。

三、幾種不同的多執行緒模式

使用者線程與核心級線程

線程的實現可以分為兩類:使用者級線程(User-LevelThread, ULT)和核心級線程(Kemel-LevelThread, KLT)。使用者線程由使用者代碼支援,核心線程由作業系統核心支援。

多執行緒模式

多執行緒模式即使用者級線程和核心級線程的不同串連方式。

(1)多對一模型(M : 1)

將多個使用者級線程映射到一個核心級線程,線程管理在使用者空間完成。
此模式中,使用者級線程對作業系統不可見(即透明)。

優點:
這種模型的好處是線程環境切換都發生在使用者空間,避免的模態切換(mode switch),從而對於效能有積極的影響。
缺點:所有的線程基於一個核心調度實體即核心線程,這意味著只有一個處理器可以被利用,在多處理環境下這是不能夠被接受的,本質上,使用者線程只解決了並發問題,但是沒有解決並行問題。

如果線程因為 I/O 操作陷入了核心態,核心態線程阻塞等待 I/O 資料,則所有的線程都將會被阻塞,使用者空間也可以使用非阻塞而 I/O,但是還是有效能及複雜度問題。

(2) 一對一模型(1:1)

將每個使用者級線程映射到一個核心級線程。

每個線程由核心調度器獨立的調度,所以如果一個線程阻塞則不影響其他的線程。
優點:在多核處理器的硬體的支援下,核心空間執行緒模式支援了真正的並行,當一個線程被阻塞後,允許另一個線程繼續執行,所以並發能力較強。

缺點:每建立一個使用者級線程都需要建立一個核心級線程與其對應,這樣建立線程的開銷比較大,會影響到應用程式的效能。

(3)多對多模型(M : N)

核心線程和使用者線程的數量比為 M : N,核心使用者空間綜合了前兩種的優點。

這種模型需要核心線程調度器和使用者空間線程調度器相互操作,本質上是多個線程被綁定到了多個核心線程上,這使得大部分的線程環境切換都發生在使用者空間,而多個核心線程又可以充分利用處理器資源。

四、goroutine機制的調度實現

goroutine機制實現了M : N的執行緒模式,goroutine機制是協程(coroutine)的一種實現,golang內建的調度器,可以讓多核CPU中每個CPU執行一個協程。

理解goroutine機制的原理,關鍵是理解Go語言scheduler的實現。

調度器是如何工作的

Go語言中支撐整個scheduler實現的主要有4個重要結構,分別是M、G、P、Sched,
前三個定義在runtime.h中,Sched定義在proc.c中。

  • Sched結構就是調度器,它維護有儲存M和G的隊列以及調度器的一些狀態資訊等。

  • M結構是Machine,系統線程,它由作業系統管理的,goroutine就是跑在M之上的;M是一個很大的結構,裡面維護小對象記憶體cache(mcache)、當前執行的goroutine、隨機數發生器等等非常多的資訊。

  • P結構是Processor,處理器,它的主要用途就是用來執行goroutine的,它維護了一個goroutine隊列,即runqueue。Processor是讓我們從N:1調度到M:N調度的重要部分。

  • G是goroutine實現的核心結構,它包含了棧,指令指標,以及其他對調度goroutine很重要的資訊,例如其阻塞的channel。

Processor的數量是在啟動時被設定為環境變數GOMAXPROCS的值,或者通過運行時調用函數GOMAXPROCS()進行設定。Processor數量固定意味著任意時刻只有GOMAXPROCS個線程在運行go代碼。

參考這篇傳播很廣的部落格:http://morsmachine.dk/go-scheduler
我們分別用三角形,矩形和圓形表示Machine Processor和Goroutine。

在單核處理器的情境下,所有goroutine運行在同一個M系統線程中,每一個M系統線程維護一個Processor,任何時刻,一個Processor中只有一個goroutine,其他goroutine在runqueue中等待。一個goroutine運行完自己的時間片後,讓出上下文,回到runqueue中。

多核處理器的情境下,為了運行goroutines,每個M系統線程會持有一個Processor。

在正常情況下,scheduler會按照上面的流程進行調度,但是線程會發生阻塞等情況,看一下goroutine對線程阻塞等的處理。

線程阻塞

當正在啟動並執行goroutine阻塞的時候,例如進行系統調用,會再建立一個系統線程(M1),當前的M線程放棄了它的Processor,P轉到新的線程中去運行。

runqueue執行完成

當其中一個Processor的runqueue為空白,沒有goroutine可以調度。它會從另外一個上下文偷取一半的goroutine。

五、對並發實現的進一步思考

Go語言的並發機制還有很多值得探討的,比如Go語言和Scala並發實現的不同,Golang CSP 和Actor模型的對比等。

瞭解並發機制的這些實現,可以協助我們更好的進行並發程式的開發,實現效能的最佳化。

關於三種多執行緒模式,可以關注一下Java語言的實現。

我們知道Java通過JVM封裝了底層作業系統的差異,而不同的作業系統可能使用不同的執行緒模式,例如Linux和windows可能使用了一對一模型,solaris和unix某些版本可能使用多對多模型。JVM規範裡沒有規定多執行緒模式的具體實現,1:1(核心線程)、N:1(使用者態線程)、M:N(混合)模型的任何一種都可以。談到Java語言的多執行緒模式,需要針對具體JVM實現,比如Oracle/Sun的HotSpot VM,預設使用1:1執行緒模式。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.