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所特有的結構,是個循環鏈表,用來存儲緩存數據
- sendx和recvx是用於記錄buf中發送和接收的index
- lock是個互斥鎖,目的是爲了保證goroutine以先進先出FIFO的方式進入結構體
- recvq和sendq分別是往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
本文參考: