Golang 語言中的 channel 實現原理
大家好,我是 frank。
歡迎大家點擊上方藍色文字「Golang 語言開發棧」關注公衆號。
設爲星標,第一時間接收推送文章。
文末掃碼,大家一起學 Golang 語言。
01
介紹
Golang 語言社區流傳一句口號:
Do not communicate by sharing memory; instead, share memory by communicating.
安全訪問共享變量是併發編程的一個難點,在 Golang 語言中,倡導通過通信共享內存,實際上就是使用 channel 傳遞共享變量,在任何給定時間,只有一個 goroutine 可以訪問該變量的值,從而避免發生數據競爭。
關於 channel 的使用方法,我們在之前的文章中「Go 語言學習之 goroutine 和 channel」介紹過,本文我們從 channel 的數據結構和執行邏輯兩個方面介紹一下它的實現原理。
02
數據結構
我們看一下 Golang 源碼中 channel 的數據結構。
$GOROOT/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 mutex
}
閱讀 channel 的源碼,可以發現 channel 的數據結構是 hchan 結構體,包含以下字段:
-
qcount 當前隊列中剩餘的元素個數
-
datasize 環形隊列的長度
-
buf 環形隊列的指針
-
elemsize 元素的大小
-
closed 關閉標識
-
elemtype 元素的類型
-
sendx 發送索引位置
-
recvx 接收索引位置
-
recvq 等待接收的協程隊列
-
sendq 等待發送的協程隊列
-
lock 互斥鎖
通過閱讀 channel 的數據結構,可以發現 channel 是使用環形隊列作爲 channel 的緩衝區,datasize
環形隊列的長度是在創建 channel 時指定的,通過 sendx
和 recvx
兩個字段分別表示環形隊列的隊尾和隊首,其中,sendx 表示數據寫入的位置,recvx 表示數據讀取的位置。
字段 recvq 和 sendq 分別表示等待接收的協程隊列和等待發送的協程隊列,當 channel 緩衝區爲空或無緩衝區時,當前協程會被阻塞,分別加入到 recvq 和 sendq 協程隊列中,等待其它協程操作 channel 時被喚醒。其中,讀阻塞的協程被寫協程喚醒,寫阻塞的協程被讀協程喚醒。
字段 elemtype 和 elemsize 表示 channel 中元素的類型和大小,需要注意的是,一個 channel 只能傳遞一種類型的值,如果需要傳遞任意類型的數據,可以使用 interface{}
類型。
字段 lock 是保證同一時間只有一個協程讀寫 channel。
03
執行邏輯
在 Golang 語言中,可以對 channel 進行讀寫操作,本小節我們分別介紹一下 channel 的讀操作和寫操作。
寫操作 channel,分爲兩種情況,第一種是 channel 的緩衝區未寫滿,直接將數據寫入緩衝區,結束 send 操作;第二種是 channel 的緩衝區已寫滿,此時,當前操作 channel 的協程將會被加入 sendq 等待發送的協程隊列,等待被讀協程喚醒。
需要注意的是,當 recvq 隊列不爲空時,證明緩衝區沒有數據,但是有協程等待讀取數據,此時,數據將不再寫入緩衝區,而是會直接把數據傳遞給 recvq 隊列中的第一個協程。
讀操作 channel,也分爲兩種情況,第一種是 channel 的緩衝區中有數據,直接讀取緩衝區中的數據,結束 recv 操作;第二種是 channel 的緩衝區中沒有數據,此時,當前操作 channel 的協程加入 recvq 等待接收的協程隊列,等待被寫協程喚醒。
需要注意的是,當 sendq 隊列不爲空時,並且緩衝區已寫滿,此時,將直接從 sendq 隊列中的第一個協程讀取數據。
當 channel 被關閉時,recvq 和 sendq 中的所有協程被喚醒,其中 recvq 中的協程讀取到的數據全部是 nil,sendq 中的協程會觸發 panic。
04
總結
本文我們在 channel 的數據結構和執行邏輯兩個方面介紹了 channel 的實現原理,其中,執行邏輯小節中,重點介紹了 channel 的讀寫操作。如果讀者朋友們想要了解創建 channel 的執行邏輯,可以閱讀源碼中的函數 func makechan(t *chantype, size int) *hchan
。
參考資料:
https://golang.org/doc/effective_go#sharing
https://blog.golang.org/codelab-share
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6Q4lqlIBWl2g-EqU-M-rHw