天天看點

Go語言并發機制初探

go 語言相比java等一個很大的優勢就是可以友善地編寫并發程式。go 語言内置了 goroutine 機制,使用goroutine可以快速地開發并發程式, 更好的利用多核處理器資源。這篇文章學習 goroutine 的應用及其排程實作。

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

goroutine的用法如下:

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

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

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

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

可以使用内置函數 make配置設定一個channel:

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

gomaxprocs的數目根據任務量配置設定就可以,但是不要大于cpu核數。

配置并行執行比較适合适合于cpu密集型、并行度比較高的情景,如果是io密集型使用多核的化會增加cpu切換帶來的性能損失。

了解了go語言的并發機制,接下來看一下goroutine 機制的具體實作。

在現代作業系統中,線程是處理器排程和配置設定的基本機關,程序則作為資源擁有的基本機關。

每個程序是由私有的虛拟位址空間、代碼、資料和其它各種系統資源組成。線程是程序内部的一個執行單元。

每一個程序至少有一個主執行線程,它無需由使用者去主動建立,是由系統自動建立的。

使用者根據需要在應用程式中建立其它線程,多個線程并發地運作于同一個程序中。

并行與并發(concurrency and parallelism)是兩個不同的概念,了解它們對于了解多線程模型非常重要。

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

并發:一個時間段内有很多的線程或程序在執行,但何時間點上都隻有一個在執行,多個線程或程序争搶時間片輪流執行

并行:一個時間段和時間點上都有多個線程或程序在執行

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

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

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

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

舉一個例子,編寫一個最簡單的程式輸出"hello world",它就是非并發的,如果在程式中增加多線程,每個線程列印一個"hello world",那麼這個程式就是并發的。運作時隻給這個程式配置設定單個cpu,這個并發程式還不是并行的,需要用多核處理器的作業系統來運作它,才能實作程式的并行。

線程的實作可以分為兩類:使用者級線程(user-levelthread, ult)和核心級線程(kemel-levelthread, klt)。使用者線程由使用者代碼支援,核心線程由作業系統核心支援。

多線程模型即使用者級線程和核心級線程的不同連接配接方式。

将多個使用者級線程映射到一個核心級線程,線程管理在使用者空間完成。

此模式中,使用者級線程對作業系統不可見(即透明)。

Go語言并發機制初探

優點:

這種模型的好處是線程上下文切換都發生在使用者空間,避免的模态切換(mode switch),進而對于性能有積極的影響。

缺點:所有的線程基于一個核心排程實體即核心線程,這意味着隻有一個處理器可以被利用,在多處理環境下這是不能夠被接受的,本質上,使用者線程隻解決了并發問題,但是沒有解決并行問題。

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

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

Go語言并發機制初探

每個線程由核心排程器獨立的排程,是以如果一個線程阻塞則不影響其他的線程。

優點:在多核處理器的硬體的支援下,核心空間線程模型支援了真正的并行,當一個線程被阻塞後,允許另一個線程繼續執行,是以并發能力較強。

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

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

Go語言并發機制初探

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

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。

Go語言并發機制初探

在單核處理器的場景下,所有goroutine運作在同一個m系統線程中,每一個m系統線程維護一個processor,任何時刻,一個processor中隻有一個goroutine,其他goroutine在runqueue中等待。一個goroutine運作完自己的時間片後,讓出上下文,回到runqueue中。

Go語言并發機制初探

多核處理器的場景下,為了運作goroutines,每個m系統線程會持有一個processor。

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

當正在運作的goroutine阻塞的時候,例如進行系統調用,會再建立一個系統線程(m1),目前的m線程放棄了它的processor,p轉到新的線程中去運作。

Go語言并發機制初探

當其中一個processor的runqueue為空,沒有goroutine可以排程。它會從另外一個上下文偷取一半的goroutine。

Go語言并發機制初探

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線程模型。