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 結構體,包含以下字段:

通過閱讀 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