golang channel 詳解
前言
CSP:不要通過共享記憶體來通訊,而要通過通訊來實現記憶體共享,它是Go 的併發哲學,基於 channel 實現。
Channel是Go中的一個核心型別,你可以把它看成一個管道,通過它併發核心單元就可以傳送或者接收資料進行通訊(communication)。
資料結構
runtime/chan.go
type hchan struct {
qcount uint // 佇列中剩餘元素
dataqsiz uint // 佇列長度,eg make(chan int64, 5), dataqsiz為5
buf unsafe.Pointer // 資料儲存環形陣列
elemsize uint16 // 每個元素的大小
closed uint32 // 是否關閉 0 未關閉
elemtype *_type // 元素型別
sendx uint // 傳送者寫入位置
recvx uint // 接受者讀資料位置
recvq waitq // 接收者佇列,儲存正在讀取channel的goroutian
sendq waitq // 傳送者佇列,儲存正在傳送channel的goroutian
lock mutex // 鎖
}
waitq是雙向連結串列,sudog為goroutian的封裝
type waitq struct {
first *sudog
last *sudog
}
make(chan int, 6)
上圖為一個長度為6,型別為int, 兩個接收者,三個傳送者的channel,當前接收者準備讀資料的位置為0,傳送者傳送資料位置為4
注意,一般情況下recvq和sendq至少有一個為空。只有一個例外,那就是同一個goroutine使用select語句向channel一邊寫資料,一邊讀資料。
channel建立
建立channel的過程實際上是初始化hchan結構。其中型別資訊和緩衝區長度由make語句傳入,buf的大小則與元素大小和緩衝區長度共同決定。
runtime/chan.go line:71
func makechan(t *chantype, size int) *hchan {
elem := t.elem
//...
var c *hchan
//建立hchan結構並分配記憶體
switch {
// 無緩衝區
case mem == 0:
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
// 元素不含指標
case elem.ptrdata == 0:
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 預設場景,結構體和buffer單獨分配記憶體
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
//元素大小
c.elemsize = uint16(elem.size)
//元素型別
c.elemtype = elem
//佇列長度
c.dataqsiz = uint(size)
//...
return c
}
寫資料
- 如果等待接收佇列recvq不為空,說明緩衝區中沒有資料或者沒有緩衝區,此時直接從recvq取出G,並把資料寫入,最後把該G喚醒,結束髮送過程;
- 如果緩衝區中有空餘位置,將資料寫入緩衝區,結束髮送過程;
- 如果緩衝區中沒有空餘位置,將待傳送資料寫入G,將當前G加入sendq,進入睡眠,等待被讀goroutine喚醒;
寫資料程式碼解析
寫資料分為阻塞寫和非阻塞寫 程式碼示例:
c := make(chan int64)
c <- 1 //阻塞寫
//非阻塞寫
select {
case c <- 1:
//do something
break
default:
//do something
}
對應原始碼裡兩個函式:
//非阻塞
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}
//阻塞
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
注意:非阻塞寫必須帶上default
chansend原始碼
// 位於 src/runtime/chan.go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 如果 channel 是 nil
if c == nil {
// 不能阻塞,直接返回 false,表示未傳送成功
if !block {
return false
}
// 當前 goroutine 被掛起
gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
throw("unreachable")
}
// 省略 debug 相關……
// 對於不阻塞的 send,快速檢測失敗場景
//
// 如果 channel 未關閉且 channel 沒有多餘的緩衝空間。這可能是:
// 1. channel 是非緩衝型的,且等待接收佇列裡沒有 goroutine
// 2. channel 是緩衝型的,但迴圈陣列已經裝滿了元素
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
// 鎖住 channel,併發安全
lock(&c.lock)
// 如果 channel 關閉了
if c.closed != 0 {
// 解鎖
unlock(&c.lock)
// 直接 panic
panic(plainError("send on closed channel"))
}
// 如果接收佇列裡有 goroutine,直接將要傳送的資料拷貝到接收 goroutine
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 對於緩衝型的 channel,如果還有緩衝空間
if c.qcount < c.dataqsiz {
// qp 指向 buf 的 sendx 位置
qp := chanbuf(c, c.sendx)
// ……
// 將資料從 ep 處拷貝到 qp
typedmemmove(c.elemtype, qp, ep)
// 傳送遊標值加 1
c.sendx++
// 如果傳送遊標值等於容量值,遊標值歸 0
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 緩衝區的元素數量加一
c.qcount++
// 解鎖
unlock(&c.lock)
return true
}
// 如果不需要阻塞,則直接返回錯誤
if !block {
unlock(&c.lock)
return false
}
// channel 滿了,傳送方會被阻塞。接下來會構造一個 sudog
// 獲取當前 goroutine 的指標
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 當前 goroutine 進入傳送等待佇列
c.sendq.enqueue(mysg)
// 當前 goroutine 被掛起
goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)
// 從這裡開始被喚醒了(channel 有機會可以傳送了)
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
// 被喚醒後,channel 關閉了。坑爹啊,panic
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
// 去掉 mysg 上繫結的 channel
mysg.c = nil
releaseSudog(mysg)
return true
}
讀資料
- 如果等待傳送佇列sendq不為空,且沒有緩衝區,直接從sendq中取出G,把G中資料讀出,最後把G喚醒,結束讀取過程;
- 如果等待傳送佇列sendq不為空,此時說明緩衝區已滿,從緩衝區中首部讀出資料,把G中資料寫入緩衝區尾部,把G喚醒,結束讀取過程;
- 如果緩衝區中有資料,則從緩衝區取出資料,結束讀取過程;
- 將當前goroutine加入recvq,進入睡眠,等待被寫goroutine喚醒;
讀資料程式碼解析
讀資料分為阻塞讀和非阻塞讀 程式碼示例:
c := make(chan int, 10)
<-c //阻塞讀
//select讀帶default為非阻塞讀
select{
case <-c:
//...
break
default:
//...
}
注意:非阻塞讀必須帶上default
接收操作有兩種寫法,一種帶 “ok”,反應 channel 是否關閉;一種不帶 “ok”,這種寫法,當接收到相應型別的零值時無法知道是真實的傳送者傳送過來的值,還是 channel 被關閉後,返回給接收者的預設型別的零值,程式碼示例:
c := make(chan int64, 5)
c <- 0
v, ok := <-c
fmt.Println(v, ok) // 0, true
close(c)
v, ok = <-c
fmt.Println(v, ok) // 0, false
最後對應原始碼裡的這四個函式:
//非阻塞讀不帶ok返回
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}
//非阻塞讀帶Ok返回
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
// TODO(khr): just return 2 values from this function, now that it is in Go.
selected, *received = chanrecv(c, elem, false)
return
}
//阻塞讀不帶ok返回
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//阻塞讀帶ok返回
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
可以看出來,最終都指向了chanrecv函式,如果有接收值,val := <-c,會把接收值放到elem的地址中,如果忽略接收值直接寫<-c,這時elem為nil
下面來看看chanrecv的程式碼
// 位於 src/runtime/chan.go
// chanrecv 函式接收 channel c 的元素並將其寫入 ep 所指向的記憶體地址。
// 如果 ep 是 nil,說明忽略了接收值。
// 如果 block == false,即非阻塞型接收,在沒有資料可接收的情況下,返回 (false, false)
// 否則,如果 c 處於關閉狀態,將 ep 指向的地址清零,返回 (true, false)
// 否則,用返回值填充 ep 指向的記憶體地址。返回 (true, true)
// 如果 ep 非空,則應該指向堆或者函式呼叫者的棧
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 省略 debug 內容 …………
// 如果是一個 nil 的 channel
if c == nil {
// 如果不阻塞,直接返回 (false, false)
if !block {
return
}
// 否則,接收一個 nil 的 channel,goroutine 掛起
gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)
// 不會執行到這裡
throw("unreachable")
}
// 在非阻塞模式下,快速檢測到失敗,不用獲取鎖,快速返回
// 當我們觀察到 channel 沒準備好接收:
// 1. 非緩衝型,等待傳送列隊 sendq 裡沒有 goroutine 在等待
// 2. 緩衝型,但 buf 裡沒有元素
// 之後,又觀察到 closed == 0,即 channel 未關閉。
// 因為 channel 不可能被重複開啟,所以前一個觀測的時候 channel 也是未關閉的,
// 因此在這種情況下可以直接宣佈接收失敗,返回 (false, false)
// 非阻塞 && ((非緩衝型 && 傳送佇列為空) || (緩衝性 && 沒有資料)) && 沒有關閉
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
// 加鎖
lock(&c.lock)
// channel 已關閉,並且迴圈陣列 buf 裡沒有元素
// 這裡可以處理非緩衝型關閉 和 緩衝型關閉但 buf 無元素的情況
// 也就是說即使是關閉狀態,但在緩衝型的 channel,
// buf 裡有元素的情況下還能接收到元素
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(unsafe.Pointer(c))
}
// 解鎖
unlock(&c.lock)
if ep != nil {
// 從一個已關閉的 channel 執行接收操作,且未忽略返回值
// 那麼接收的值將是一個該型別的零值
// typedmemclr 根據型別清理相應地址的記憶體
typedmemclr(c.elemtype, ep)
}
// 從一個已關閉的 channel 接收,selected 會返回true
return true, false
}
// 等待傳送佇列裡有 goroutine 存在,說明 buf 是滿的
// 這有可能是:
// 1. 非緩衝型的 channel
// 2. 緩衝型的 channel,但 buf 滿了
// 針對 1,直接進行記憶體拷貝(從 sender goroutine -> receiver goroutine)
// 針對 2,接收到迴圈陣列頭部的元素,並將傳送者的元素放到迴圈陣列尾部
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// 緩衝型,buf 裡有元素,可以正常接收
if c.qcount > 0 {
// 直接從迴圈陣列裡找到要接收的元素
qp := chanbuf(c, c.recvx)
// …………
// 程式碼裡,沒有忽略要接收的值,不是 "<- ch",而是 "val <- ch",ep 指向 val
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清理掉迴圈陣列裡相應位置的值
typedmemclr(c.elemtype, qp)
// 接收遊標向前移動
c.recvx++
// 接收遊標歸零
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// buf 陣列裡的元素個數減 1
c.qcount--
// 解鎖
unlock(&c.lock)
return true, true
}
if !block {
// 非阻塞接收,解鎖。selected 返回 false,因為沒有接收到值
unlock(&c.lock)
return false, false
}
// 接下來就是要被阻塞的情況了
// 構造一個 sudog
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 待接收資料的地址儲存下來
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.param = nil
// 進入channel 的等待接收佇列
c.recvq.enqueue(mysg)
// 將當前 goroutine 掛起
goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)
// 被喚醒了,接著從這裡繼續執行一些掃尾工作
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
closed := gp.param == nil
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, !closed
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 如果是非緩衝型的 channel
if c.dataqsiz == 0 {
if raceenabled {
racesync(c, sg)
}
// 未忽略接收的資料
if ep != nil {
// 直接拷貝資料,從 sender goroutine -> receiver goroutine
recvDirect(c.elemtype, sg, ep)
}
} else {
// 緩衝型的 channel,但 buf 已滿。
// 將迴圈陣列 buf 隊首的元素拷貝到接收資料的地址
// 將傳送者的資料入隊。實際上這時 revx 和 sendx 值相等
// 找到接收遊標
qp := chanbuf(c, c.recvx)
// …………
// 將接收遊標處的資料拷貝給接收者
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 將傳送者資料拷貝到 buf
typedmemmove(c.elemtype, qp, sg.elem)
// 更新遊標值
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx
}
sg.elem = nil
gp := sg.g
// 解鎖
unlockf()
gp.param = unsafe.Pointer(sg)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 喚醒傳送的 goroutine。需要等到排程器的光臨
goready(gp, skip+1)
}
關閉channel
close 邏輯比較簡單,對於一個 channel,recvq 和 sendq 中分別儲存了阻塞的傳送者和接收者。關閉 channel 後,對於等待接收者而言,會收到一個相應型別的零值。對於等待傳送者,會直接 panic。所以,在不瞭解 channel 還有沒有接收者的情況下,不能貿然關閉 channel。
close 函式先上一把大鎖,接著把所有掛在這個 channel 上的 sender 和 receiver 全都連成一個 sudog 連結串列,再解鎖。最後,再將所有的 sudog 全都喚醒。
注意:關閉已經關閉的channel或者往一個關閉的channel中傳送資料會發生panic
關閉原則:
一般原則上使用通道是不允許接收方關閉通道和 不能關閉一個有多個併發傳送者的通道。 換而言之, 你只能在傳送方的 goroutine 中關閉只有該傳送方的通道。
關閉程式碼解析
func closechan(c *hchan) {
// 關閉一個 nil channel,panic
if c == nil {
panic(plainError("close of nil channel"))
}
// 上鎖
lock(&c.lock)
// 如果 channel 已經關閉
if c.closed != 0 {
unlock(&c.lock)
// panic
panic(plainError("close of closed channel"))
}
// …………
// 修改關閉狀態
c.closed = 1
var glist *g
// 將 channel 所有等待接收佇列的裡 sudog 釋放
for {
// 從接收佇列裡出隊一個 sudog
sg := c.recvq.dequeue()
// 出隊完畢,跳出迴圈
if sg == nil {
break
}
// 如果 elem 不為空,說明此 receiver 未忽略接收資料
// 給它賦一個相應型別的零值
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 取出 goroutine
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, unsafe.Pointer(c))
}
// 相連,形成連結串列
gp.schedlink.set(glist)
glist = gp
}
// 將 channel 等待傳送佇列裡的 sudog 釋放
// 如果存在,這些 goroutine 將會 panic
for {
// 從傳送佇列裡出隊一個 sudog
sg := c.sendq.dequeue()
if sg == nil {
break
}
// 傳送者會 panic
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, unsafe.Pointer(c))
}
// 形成連結串列
gp.schedlink.set(glist)
glist = gp
}
// 解鎖
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
// 遍歷連結串列
for glist != nil {
// 取最後一個
gp := glist
// 向前走一步,下一個喚醒的 g
glist = glist.schedlink.ptr()
gp.schedlink = 0
// 喚醒相應 goroutine
goready(gp, 3)
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結