Go語言併發機制

 

Go語言中的併發

使用goroutine編程

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

goroutine的用法以下:golang

//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 機制的具體實現。性能

 

 

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併發模型最基本的形式。

 

 

 

幾種不一樣的多線程模型

用戶線程與內核級線程

線程的實現能夠分爲兩類:用戶級線程(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中。

以上這個圖講的是兩個線程(內核線程)的狀況。一個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中偷一半!

 

 

 

參考:

https://yq.aliyun.com/articles/72365

http://morsmachine.dk/go-scheduler

https://studygolang.com/articles/11825

相關文章
相關標籤/搜索