Golang使用Groutine和channels實現了CSP(Communicating Sequential Processes)模型,channles在goroutine的通訊和同步中承擔着重要的角色。在GopherCon 2017中,Golang專家Kavya深刻介紹了 Go Channels 的內部機制,以及運行時調度器和內存管理系統是如何支持Channel的,本文根據Kavya的ppt學習和分析一下go channels的原理,但願可以對之後正確高效使用golang的併發帶來一些啓發。c++
以一個簡單的channel應用開始,使用goroutine和channel實現一個任務隊列,並行處理多個任務。git
func main(){ //帶緩衝的channel ch := make(chan Task, 3) //啓動固定數量的worker for i := 0; i< numWorkers; i++ { go worker(ch) } //發送任務給worker hellaTasks := getTaks() for _, task := range hellaTasks { ch <- task } ... } func worker(ch chan Task){ for { //接受任務 task := <- ch process(task) } }
從上面的代碼能夠看出,使用golang的goroutine和channel能夠很容易的實現一個生產者-消費者模式的任務隊列,相比java, c++簡潔了不少。channel能夠自然的實現了下面四個特性: github
- goroutine安全
- 在不一樣的goroutine之間存儲和傳輸值
- 提供FIFO語義(buffered channel提供)
- 可讓goroutine block/unblock
那麼channel是怎麼實現這些特性的呢?下面咱們看看當咱們調用make來生成一個channel的時候都作了些什麼。golang
make chan
上述任務隊列的例子第三行,使用make建立了一個長度爲3的帶緩衝的channel,channel在底層是一個hchan結構體,位於src/runtime/chan.go
裏。其定義以下:緩存
type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G's status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex }
make函數在建立channel的時候會在該進程的heap區申請一塊內存,建立一個hchan結構體,返回執行該內存的指針,因此獲取的的ch變量自己就是一個指針,在函數之間傳遞的時候是同一個channel。 安全
hchan結構體使用一個環形隊列來保存groutine之間傳遞的數據(若是是緩存channel的話),使用兩個list保存像該chan發送和從改chan接收數據的goroutine,還有一個mutex來保證操做這些結構的安全。數據結構
發送和接收
向channel發送和從channel接收數據主要涉及hchan裏的四個成員變量,借用Kavya ppt裏的圖示,來分析發送和接收的過程。
仍是之前面的任務隊列爲例:併發
//G1 func main(){ ... for _, task := range hellaTasks { ch <- task //sender } ... } //G2 func worker(ch chan Task){ for { //接受任務 task := <- ch //recevier process(task) } }
其中G1是發送者,G2是接收,由於ch是長度爲3的帶緩衝channel,初始的時候hchan結構體的buf爲空,sendx和recvx都爲0,當G1向ch裏發送數據的時候,會首先對buf加鎖,而後將要發送的數據copy到buf裏,並增長sendx的值,最後釋放buf的鎖。而後G2消費的時候首先對buf加鎖,而後將buf裏的數據copy到task變量對應的內存裏,增長recvx,最後釋放鎖。整個過程,G1和G2沒有共享的內存,底層經過hchan結構體的buf,使用copy內存的方式進行通訊,最後達到了共享內存的目的,這徹底符合CSP的設計理念 函數
Do not comminute by sharing memory;instead, share memory by communicating
通常狀況下,G2的消費速度應該是慢於G1的,因此buf的數據會愈來愈多,這個時候G1再向ch裏發送數據,這個時候G1就會阻塞,那麼阻塞究竟是發生了什麼呢?
Goroutine Pause/Resume
goroutine是Golang實現的用戶空間的輕量級的線程,有runtime調度器調度,與操做系統的thread有多對一的關係,相關的數據結構以下圖:
其中M是操做系統的線程,G是用戶啓動的goroutine,P是與調度相關的context,每一個M都擁有一個P,P維護了一個可以運行的goutine隊列,用於該線程執行。
當G1向buf已經滿了的ch發送數據的時候,當runtine檢測到對應的hchan的buf已經滿了,會通知調度器,調度器會將G1的狀態設置爲waiting, 移除與線程M的聯繫,而後從P的runqueue中選擇一個goroutine在線程M中執行,此時G1就是阻塞狀態,可是不是操做系統的線程阻塞,因此這個時候只用消耗少許的資源。
那麼G1設置爲waiting狀態後去哪了?怎們去resume呢?咱們再回到hchan結構體,注意到hchan有個sendq的成員,其類型是waitq,查看源碼以下:
type hchan struct { ... recvq waitq // list of recv waiters sendq waitq // list of send waiters ... } // type waitq struct { first *sudog last *sudog }
實際上,當G1變爲waiting狀態後,會建立一個表明本身的sudog的結構,而後放到sendq這個list中,sudog結構中保存了channel相關的變量的指針(若是該Goroutine是sender,那麼保存的是待發送數據的變量的地址,若是是receiver則爲接收數據的變量的地址,之因此是地址,前面咱們提到在傳輸數據的時候使用的是copy的方式)
當G2從ch中接收一個數據時,會通知調度器,設置G1的狀態爲runnable,而後將加入P的runqueue裏,等待線程執行.
wait empty channel
前面咱們是假設G1先運行,若是G2先運行會怎麼樣呢?若是G2先運行,那麼G2會從一個empty的channel裏取數據,這個時候G2就會阻塞,和前面介紹的G1阻塞同樣,G2也會建立一個sudog結構體,保存接收數據的變量的地址,可是該sudog結構體是放到了recvq列表裏,當G1向ch發送數據的時候,runtime並無對hchan結構體題的buf進行加鎖,而是直接將G1裏的發送到ch的數據copy到了G2 sudog裏對應的elem指向的內存地址!
總結
Golang的一大特點就是其簡單搞笑的自然併發機制,使用goroutine和channel實現了CSP模型。理解channel的底層運行機制對靈活運用golang開發併發程序有很大的幫助,看了Kavya的分享,而後結合golang runtime相關的源碼(源碼開源而且也是golang實現簡直良心!),對channel的認識更加的深入,固然還有一些地方存在一些疑問,好比goroutine的調度實現相關的,仍是要潛心膜拜大神們的源碼!