1. 引言
"Do not communicate by sharing memory; instead, share memory by communicating." (不要通過共享內存來通信,而應通過通信來共享內存。) 這是 Go 語言并發設計的核心哲學。而 channel
正是實現這一哲學的核心工具。
Channel 為 Goroutine 之間的通信提供了安全的、同步的機制。它究竟是如何在底層保證并發安全和實現阻塞/非阻塞操作的?本文將深入其源碼,揭示 channel
的內部奧秘。
2. Channel 的核心數據結構
在 Go 的 runtime/chan.go
源碼中,channel
的底層實現是一個名為 hchan
的結構體。其核心字段如下(有簡化):
// src/runtime/chan.go
type hchan struct {qcount uint // channel 中當前的元素個數dataqsiz uint // channel 的容量(環形隊列的大小)buf unsafe.Pointer // 指向容量大小為 dataqsiz 的環形隊列elemsize uint16 // channel 中元素的大小closed uint32 // 標記 channel 是否關閉sendx uint // 環形隊列的發送索引recvx uint // 環形隊列的接收索引recvq waitq // 等待接收的 goroutine 隊列 (sudog 鏈表)sendq waitq // 等待發送的 goroutine 隊列 (sudog 鏈表)lock mutex // 保證 channel 操作的原子性
}type waitq struct {first *sudoglast *sudog
}
核心組件解析:
buf
(環形隊列): 對于帶緩沖的 channel,buf
是一個環形隊列,用于存儲元素。發送和接收操作通過移動sendx
和recvx
索引來完成。lock
(互斥鎖): channel 的所有操作(發送、接收、關閉)都必須先獲取這個鎖,這保證了其并發安全性。sendq
和recvq
(等待隊列): 這是 channel 實現阻塞和喚醒的關鍵。當一個 goroutine 嘗試向一個已滿的 channel 發送數據時,它會被打包成一個
sudog
(goroutine 在運行時的表示)并加入到sendq
等待隊列中,然后該 goroutine 會被掛起(park)。當一個 goroutine 嘗試從一個空的 channel 接收數據時,它也會被加入到
recvq
等待隊列中并被掛起。
3. Channel 的操作原理
3.1 發送操作 (ch <- data
)
加鎖:
lock.Lock()
。檢查
closed
標志:如果 channel 已關閉,直接panic
。檢查
recvq
:如果接收等待隊列recvq
不為空,說明有 goroutine 正在等待接收數據。這是無緩沖 channel或空緩沖 channel的接收者。
直接將要發送的數據拷貝給等待的 goroutine。
喚醒(
gounpark
)該 goroutine。解鎖,發送完成。
檢查
buf
:如果buf
(環形隊列) 還有空間 (qcount < dataqsiz
)。將數據拷貝到
buf
的sendx
位置。sendx
索引遞增。qcount
遞增。解鎖,發送完成。
阻塞發送:如果
recvq
為空且buf
已滿。將當前 goroutine 和要發送的數據打包成
sudog
。加入
sendq
發送等待隊列。掛起當前 goroutine (
gopark
),并解鎖。goroutine 會在此等待,直到有接收者將其喚醒。
3.2 接收操作 (<-ch
)
加鎖:
lock.Lock()
。檢查
sendq
:如果發送等待隊列sendq
不為空。這通常發生在無緩沖 channel或滿緩沖 channel。
從
sendq
中取出一個等待的 goroutine。如果
buf
為空,直接從該 goroutine 中取出數據。如果
buf
已滿,先將buf
的隊首元素取出作為返回值,然后將等待 goroutine 的數據存入buf
隊尾。喚醒該發送 goroutine。
解鎖,接收完成。
檢查
buf
:如果buf
中有數據 (qcount > 0
)。從
buf
的recvx
位置取出數據。recvx
索引遞增。qcount
遞減。解鎖,接收完成。
檢查
closed
標志:如果 channel 已關閉且buf
為空,立即返回元素類型的零值。阻塞接收:如果上述條件都不滿足。
將當前 goroutine 打包成
sudog
。加入
recvq
接收等待隊列。掛起當前 goroutine (
gopark
) 并解鎖。
4. select
的實現
select
語句的實現更為復雜,它會將涉及到的所有 case
構建成一個 scase
數組,然后通過 selectgo
函數執行以下邏輯:
隨機輪詢:打亂
scase
數組的順序,防止優先級問題。非阻塞檢查:遍歷所有
case
,檢查是否有任何一個 channel 可以立即進行非阻塞的發送或接收。如果有,則執行該操作并返回。阻塞等待:如果所有
case
都無法立即完成,將當前 goroutine 加入到所有相關 channel 的等待隊列中,然后掛起。喚醒:當任何一個 channel 的操作條件滿足時(例如,有數據被發送進來),對應的 channel 會喚醒這個等待的 goroutine。goroutine 被喚醒后,會完成相應的
case
操作。
5. 總結
Go channel 的底層是一個由互斥鎖、環形隊列和兩個等待隊列(sudog
鏈表)組成的精密結構。正是通過 lock
保證了并發安全,通過 sendq
和 recvq
配合調度器的 gopark
和 gounpark
,實現了 goroutine 之間的同步與通信。理解這一機制,有助于我們更深刻地運用 Go 的并發能力。