詳細解讀go語言中的chnanel

cooper發表於2021-09-09

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不允許併發讀寫
}

waitqsudog 的一個雙向連結串列

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。

相關文章