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。