Channel
底層資料結構
type hchan struct {
qcount uint // 當前佇列中剩餘元素個數
dataqsiz uint // 環形佇列長度,即可以存放的元素個數
buf unsafe.Pointer // 環形佇列指標
elemsize uint16 // 每個元素的大小
closed uint32 // 標識關閉狀態
elemtype *_type // 元素型別
sendx uint // 佇列下標,指示元素寫入時存放到佇列中的位置
recvx uint // 佇列下標,指示元素從佇列的該位置讀出
recvq waitq // 等待讀訊息的goroutine佇列
sendq waitq // 等待寫訊息的goroutine佇列
lock mutex // 互斥鎖,chan不允許併發讀寫
}
waitq
是 sudog
的一個雙向連結串列
1. type waitq struct {
2. first *sudog
3. last *sudog
4. }
而 sudog
實際上是對 goroutine 的一個封裝,表示一個在等待佇列中的goroutine,該結構
儲存了兩個分別指向前後sudog的指標用來構成連結串列
傳送資料
- 如果當前channel的recvq上存在已經被阻塞的Goroutine(也就是說有goroutine在等待讀訊息),那麼會直接將資料傳送給當前的Goroutine並將其設定成下一個執行的Goroutine(設定處理器runnext屬性,不會立刻排程)
- 如果channel存在緩衝區並且還有空餘位置,會直接將資料儲存到快取區sendx所在的位置上
- 如果不滿足上述兩種情況,會建立一個sudog結構並將其加入channel的sendq佇列中,當前Goroutine陷入阻塞等待其他協程從Channel接收資料
接收資料
-
如果Channel為空,那麼會直接讓出處理器的使用權。
-
如果Channel已經關閉並且快取區沒有任何資料,會直接返回
-
如果Channel的sendq佇列中存在掛起的Goroutine(說明有阻塞傳送的goroutine),根據緩衝區的大小分別處理不同的情況:
如果 Channel 不存在緩衝區, 將 Channel 傳送佇列中 Goroutine 儲存的資料拷貝到目標記憶體地址中;
如果 Channel 存在緩衝區,將佇列中的資料拷貝到接收方的記憶體地址;將傳送佇列頭的資料拷貝到緩衝區中,釋放一個阻塞的傳送方;
-
如果Channel的緩衝區存在資料(沒有阻塞的傳送Goroutine),會將緩衝區中的資料拷貝到接收方的記憶體地址、清除佇列中的資料並完成收尾工作。
-
當 Channel 的傳送佇列中不存在等待的 Goroutine 並且緩衝區中也不存在任何資料時,會使用
runtime.sudog
將當前 Goroutine 包裝成一個處於等待狀態的 Goroutine 將其加入到接收佇列中並陷入休眠等待排程器的喚醒;
關閉通道
當 Channel 是一個空指標或者已經被關閉時,Go 語言執行時都會直接崩潰並丟擲異常
處理完了這些異常的情況之後就可以開始執行關閉 Channel 的邏輯了,close 函式先上一把大鎖,接著把所有掛在這個 channel 上的 sender 和 receiver 全都連成一個 sudog 連結串列,再解鎖。最後,再將所有的 sudog 全都喚醒。
喚醒之後,sender 會檢測到channel已經關閉,panic。從一個有緩衝的 channel 裡讀資料,當 channel 被關閉,依然能讀出有效值。只有當返回的 ok 為 false 時,讀出的資料才是無效的,為對應型別的零值。
x, ok := <-ch
產生panic的情況
向一個關閉的 channel 進行寫操作;關閉一個 nil 的 channel;重複關閉一個 channel。
總結
channel快取區是由迴圈佇列實現的
channel的等待佇列是一個雙向連結串列
channel 的傳送和接收操作本質上都是 “值的拷貝”
從一個有緩衝的 channel 裡讀資料,當 channel 被關閉,依然能讀出有效值。發資料會panic。