Go 語言比起Java等一個很大的優點就是可以方便地寫並發程式。 Go 語言內建了 goroutine 機制,使用goroutine可以快速地開發並發程序, 更好的利用多核心處理器資源。這篇文章學習 goroutine 的應用及其調度實現。
一、Go語言對同時發生的支援
使用goroutine程式設計 (建議: go影片教學)
使用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 0 s := make(chan string, 3) // non-zero capacity r := make(<-chan bool) // can only read from w := 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程式碼。
我們分別用三角形,矩形和圓形表示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執行緒模型。
以上是Go語言並發機製圖文詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!