Go併發原理

Go語言是爲併發而生的語言,Go語言是爲數很少的在語言層面實現併發的語言;也正是Go語言的併發特性,吸引了全球無數的開發者。java

併發(concurrency)和並行(parallellism)

併發(concurrency):兩個或兩個以上的任務在一段時間內被執行。咱們沒必要care這些任務在某一個時間點是不是同時執行,可能同時執行,也可能不是,咱們只關心在一段時間內,哪怕是很短的時間(一秒或者兩秒)是否執行解決了兩個或兩個以上任務。golang

**並行(parallellism):**兩個或兩個以上的任務在同一時刻被同時執行。編程

併發說的是邏輯上的概念,而並行,強調的是物理運行狀態。併發「包含」並行。數組

(詳情請見:Rob Pike 的PPT安全

Go的CSP併發模型

Go實現了兩種併發形式。第一種是你們廣泛認知的:多線程共享內存。其實就是Java或者C++等語言中的多線程開發。另一種是Go語言特有的,也是Go語言推薦的:CSP(communicating sequential processes)併發模型。微信

CSP併發模型是在1970年左右提出的概念,屬於比較新的概念,不一樣於傳統的多線程經過共享內存來通訊,CSP講究的是「以通訊的方式來共享內存」。數據結構

請記住下面這句話: Do not communicate by sharing memory; instead, share memory by communicating. 「不要以共享內存的方式來通訊,相反,要經過通訊來共享內存。」多線程

普通的線程併發模型,就是像Java、C++、或者Python,他們線程間通訊都是經過共享內存的方式來進行的。很是典型的方式就是,在訪問共享數據(例如數組、Map、或者某個結構體或對象)的時候,經過鎖來訪問,所以,在不少時候,衍生出一種方便操做的數據結構,叫作「線程安全的數據結構」。例如Java提供的包"java.util.concurrent"中的數據結構。Go中也實現了傳統的線程併發模型。架構

Go的CSP併發模型,是經過goroutinechannel來實現的。併發

  • goroutine 是Go語言中併發的執行單位。有點抽象,其實就是和傳統概念上的」線程「相似,能夠理解爲」線程「。
  • channel是Go語言中各個併發結構體(goroutine)以前的通訊機制。 通俗的講,就是各個goroutine之間通訊的」管道「,有點相似於Linux中的管道。

生成一個goroutine的方式很是的簡單:Go一下,就生成了。

go f();
複製代碼

通訊機制channel也很方便,傳數據用channel <- data,取數據用<-channel

在通訊過程當中,傳數據channel <- data和取數據<-channel必然會成對出現,由於這邊傳,那邊取,兩個goroutine之間纔會實現通訊。

並且無論傳仍是取,必阻塞,直到另外的goroutine傳或者取爲止。

有兩個goroutine,其中一個發起了向channel中發起了傳值操做。(goroutine爲矩形,channel爲箭頭)

左邊的goroutine開始阻塞,等待有人接收。

這時候,右邊的goroutine發起了接收操做。

右邊的goroutine也開始阻塞,等待別人傳送。

這時候,兩邊goroutine都發現了對方,因而兩個goroutine開始一傳,一收。

這即是Golang CSP併發模型最基本的形式。

Go併發模型的實現原理

咱們先從線程講起,不管語言層面何種併發模型,到了操做系統層面,必定是以線程的形態存在的。而操做系統根據資源訪問權限的不一樣,體系架構可分爲用戶空間和內核空間;內核空間主要操做訪問CPU資源、I/O資源、內存資源等硬件資源,爲上層應用程序提供最基本的基礎資源,用戶空間呢就是上層應用程序的固定活動空間,用戶空間不能夠直接訪問資源,必須經過「系統調用」、「庫函數」或「Shell腳本」來調用內核空間提供的資源。

咱們如今的計算機語言,能夠狹義的認爲是一種「軟件」,它們中所謂的「線程」,每每是用戶態的線程,和操做系統自己內核態的線程(簡稱KSE),仍是有區別的。

線程模型的實現,能夠分爲如下幾種方式:

用戶級線程模型

如圖所示,多個用戶態的線程對應着一個內核線程,程序線程的建立、終止、切換或者同步等線程工做必須自身來完成。

內核級線程模型

這種模型直接調用操做系統的內核線程,全部線程的建立、終止、切換、同步等操做,都由內核來完成。C++就是這種。

兩級線程模型

這種模型是介於用戶級線程模型和內核級線程模型之間的一種線程模型。這種模型的實現很是複雜,和內核級線程模型相似,一個進程中能夠對應多個內核級線程,可是進程中的線程不和內核線程一一對應;這種線程模型會先建立多個內核級線程,而後用自身的用戶級線程去對應建立的多個內核級線程,自身的用戶級線程須要自己程序去調度,內核級的線程交給操做系統內核去調度。

Go語言的線程模型就是一種特殊的兩級線程模型。暫且叫它「MPG」模型吧。

Go線程實現模型MPG

M指的是Machine,一個M直接關聯了一個內核線程。 P指的是"processor",表明了M所需的上下文環境,也是處理用戶級代碼邏輯的處理器。 G指的是Goroutine,其實本質上也是一種輕量級的線程。

三者關係以下圖所示:

以上這個圖講的是兩個線程(內核線程)的狀況。一個M會對應一個內核線程,一個M也會鏈接一個上下文P,一個上下文P至關於一個「處理器」,一個上下文鏈接一個或者多個Goroutine。P(Processor)的數量是在啓動時被設置爲環境變量GOMAXPROCS的值,或者經過運行時調用函數runtime.GOMAXPROCS()進行設置。Processor數量固定意味着任意時刻只有固定數量的線程在運行go代碼。Goroutine中就是咱們要執行併發的代碼。圖中P正在執行的Goroutine爲藍色的;處於待執行狀態的Goroutine爲灰色的,灰色的Goroutine造成了一個隊列runqueues

三者關係的宏觀的圖爲:

拋棄P(Processor)

你可能會想,爲何必定須要一個上下文,咱們能不能直接除去上下文,讓Goroutinerunqueues掛到M上呢?答案是不行,須要上下文的目的,是讓咱們能夠直接放開其餘線程,當遇到內核線程阻塞的時候。

一個很簡單的例子就是系統調用sysall,一個線程確定不能同時執行代碼和系統調用被阻塞,這個時候,此線程M須要放棄當前的上下文環境P,以即可以讓其餘的Goroutine被調度執行。

如上圖左圖所示,M0中的G0執行了syscall,而後就建立了一個M1(也有可能自己就存在,沒建立),(轉向右圖)而後M0丟棄了P,等待syscall的返回值,M1接受了P,將·繼續執行Goroutine隊列中的其餘Goroutine

當系統調用syscall結束後,M0會「偷」一個上下文,若是不成功,M0就把它的Gouroutine G0放到一個全局的runqueue中,而後本身放到線程池或者轉入休眠狀態。全局runqueue是各個P在運行完本身的本地的Goroutine runqueue後用來拉取新goroutine的地方。P也會週期性的檢查這個全局runqueue上的goroutine,不然,全局runqueue上的goroutines可能得不到執行而餓死。

均衡的分配工做

按照以上的說法,上下文P會按期的檢查全局的goroutine 隊列中的goroutine,以便本身在消費掉自身Goroutine隊列的時候有事可作。假如全局goroutine隊列中的goroutine也沒了呢?就從其餘運行的中的P的runqueue裏偷。

每一個P中的Goroutine不一樣致使他們運行的效率和時間也不一樣,在一個有不少P和M的環境中,不能讓一個P跑完自身的Goroutine就沒事可作了,由於或許其餘的P有很長的goroutine隊列要跑,得須要均衡。 該如何解決呢?

Go的作法倒也直接,從其餘P中偷一半!

參考文獻: The Go scheduler 《Go併發編程初版》

更多精彩內容,請關注個人微信公衆號 互聯網技術窩 或者加微信共同探討交流:

相關文章
相關標籤/搜索