Golang-channel底層實現精要

Golang-channel底層實現精要

一.channel 背景知識

  • channel是Go語言內置的核心類型,可以將其看做一個管道,channel和goroutine一起爲go併發編程提供了最優雅和便利的方案
  • 在Go中有一句經典名言,永遠不要通過共享內存來通信,而是要通過通信來共享內存,channel便是用於實現goroutine間通信的
  • channel提供了三種類型
    • 單向只能發送:chan<- struct{} 只能發送struct (箭頭指向channel,則代表發送)
    • 單向只能接收:<-chan struct{} 只能從chan裏接收struct (箭頭遠離channel,則代表接收)
    • 雙向即可發送也可接收:chan string 既能接收也能發送
  • nil是channel的零值,對值是nil的channel發送和接收總是會阻塞

二.channel 底層實現

1.channel底層結構

簡要說明:

  • buf是帶緩衝的channle所特有的結構,是個循環鏈表,用來存儲緩存數據
  • sendxrecvx是用於記錄buf中發送和接收的index
  • lock是個互斥鎖,目的是爲了保證goroutine以先進先出FIFO的方式進入結構體
  • recvqsendq分別是往channel接收或發送數據的goroutine所抽象出來的數據結構,是個雙向鏈表

channel結構體的源碼位於/runtime/chan.go中,結構體爲hchan,源碼如下(版本1.11)

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
}

2.channel實現原理

1) channel 創建

ch := make(chan int, 3)

因爲 channel 的創建全部調用的 mallocgc(),在堆上開闢的內存空間,所以channel 本身會被 GC 自動回收。回收的條件是沒有goroutine引用

簡要說明:

  • 創建channel實際上就是在內存中實例化了一個hchan結構體,並返回一個chan指針
  • channle在函數間傳遞都是使用的這個指針,這就是爲什麼函數傳遞中無需使用channel的指針,而是直接用channel就行了,因爲channel本身就是一個指針

2) channel 發送數據

以有緩衝的channel爲例

ch <- 1
ch <- 2
ch <- 3

簡要說明:

  • 發送數據前,會先鎖住hchan這個結構體
  • 然後逐步往buf中填充數據(從goroutine中copy數據到buf),然後解鎖
  • 注意sendx的變化,其記錄了發送數據的index

3) channel 接收數據

<-ch
<-ch
<-ch

簡要說明:

  • 接收數據前,同樣會先鎖住hchan這個結構體
  • 然後逐步往buf中獲取數據(buf中copy數據到goroutine),然後解鎖
  • 注意recvx的變化,其記錄了接收數據的index

4) channel存儲滿了,底層如何處理的?

我們都知道,當channle緩存滿了的時候,會阻塞當前goroutine,但是,這是如何實現的呢?

  • goroutine的阻塞操作,實際上是調用send (ch <- xx)或者recv ( <-ch)主動觸發的
//goroutine1 中,記做G1
ch := make(chan int, 3)
ch <- 1
ch <- 1
ch <- 1

這個時候,G1在正常運行,當再次調度send操作的時候,會主動調用Go的調度器,讓當前協程G1等待,並且讓出內核線程M,交給其他G使用

同時,G1也會被抽象成含有G1指針和send元素的sudog結構體,保存到*sendq中等待被喚醒,那G1什麼時候被喚醒呢?在有其他協程(G2)接收數據後被喚醒

G2執行了recv,於是會發生以下操作:

  • 1)G2從buf中取出數據
  • 2)channel從sendq中推出G1,將G1當時的send數據推到buf中
  • 3)調用Go的調度器scheduler,喚醒G1,並把G1放到可運行的Goroutine隊列中

5) channel是空的,底層如何處理的?

當channel中無數據時,先執行G2的接收數據操作,G2會阻塞,這又是如何實現的呢?其實跟上面相差不大,可以順着思路反推

  • 1)G2首先會主動調用Go調度器,讓G2等待,並且讓出M,交給其他G使用
  • 2)然後G2還好被抽象成含有G2指針和recv空元素的sudog結構體,保存到recvq中等待被喚醒

此時,如果G1向channel中發送數據,會發生一個有意思的事情:

  • G1並沒有鎖住channel,然後將數據放入buf中,而是直接將數據從G1 copy到了 G2,這種方式非常好
  • 這樣的話,在喚醒G2的過程中,G2無需再獲得channel的鎖,然後從buf中取數據,減少了內存cpoy,提高了效率

  • 通過Go的調度器喚醒G2,將G2加入到GPM模型中P的本地可運行G隊列中

3.總結

  • channel緩衝器滿或空,其底層的處理都非常的精妙,主動調用調度器,阻塞當前G,將M交給其他G使用,然後將G指針和其他數據組裝成sudog,加入recvq或者sendq隊列,等待被調度,
  • 喚醒的流程也非常有趣,當G2接收但channel空阻塞時,G1發送數據,採用了直接copy方式,並沒有鎖住channel,將數據放入buf,而是直接從G1 複製到G2,減少了內存copy

本文參考:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章