golang-channel詳解

xuefeng發表於2021-06-26

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
}

寫資料

  1. 如果等待接收佇列recvq不為空,說明緩衝區中沒有資料或者沒有緩衝區,此時直接從recvq取出G,並把資料寫入,最後把該G喚醒,結束髮送過程;
  2. 如果緩衝區中有空餘位置,將資料寫入緩衝區,結束髮送過程;
  3. 如果緩衝區中沒有空餘位置,將待傳送資料寫入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
}

讀資料

  1. 如果等待傳送佇列sendq不為空,且沒有緩衝區,直接從sendq中取出G,把G中資料讀出,最後把G喚醒,結束讀取過程;
  2. 如果等待傳送佇列sendq不為空,此時說明緩衝區已滿,從緩衝區中首部讀出資料,把G中資料寫入緩衝區尾部,把G喚醒,結束讀取過程;
  3. 如果緩衝區中有資料,則從緩衝區取出資料,結束讀取過程;
  4. 將當前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 協議》,轉載必須註明作者和本文連結