Golang channel底層是如何實現的?(深度好文)

golang架构师k哥發表於2024-07-05

Hi 你好,我是k哥。大廠搬磚6年的後端程式設計師。

我們知道,Go語言為了方便使用者,提供了簡單、安全的協程資料同步和通訊機制,channel。那我們知道channel底層是如何實現的嗎?今天k哥就來聊聊channel的底層實現原理。同時,為了驗證我們是否掌握了channel的實現原理,本文也收集了channel的高頻面試題,理解了原理,面試題自然不在話下。

1 原理

預設情況下,讀寫未就緒的channel(讀沒有資料的channel,或者寫緩衝區已滿的channel)時,協程會被阻塞。

但是當讀寫channel操作和select搭配使用時,即使channel未就緒,也可以執行其它分支,當前協程不會被阻塞。

ch := make(chan int)
select{
  case <- ch:
  default:
}

本文主要介紹channel的阻塞模式,和select搭配使用的非阻塞模式,後續會另起一篇介紹。

1.1 資料結構

channel涉及到的核心資料結構包含3個。

hchan

// channel
type hchan struct {
    // 迴圈佇列
    qcount   uint           // 通道中資料個數
    dataqsiz uint           // buf長度
    buf      unsafe.Pointer // 陣列指標
    sendx    uint   // send index
    recvx    uint   // receive index
    elemsize uint16 // 元素大小
    elemtype *_type // 元素型別
    
    closed   uint32 // 通道關閉標誌
    
    recvq    waitq  // 由雙向連結串列實現的recv waiters佇列
    sendq    waitq  // 由雙向連結串列實現的send waiters佇列
    lock mutex
}

hchan是channel底層的資料結構,其核心是由陣列實現的一個環形緩衝區:

  1. qcount 通道中資料個數

  2. dataqsiz 陣列長度

  3. buf 指向陣列的指標,陣列中儲存往channel傳送的資料

  4. sendx 傳送元素到陣列的index

  5. recvx 從陣列中接收元素的index

  6. elemsize channel中元素型別的大小

  7. elemtype channel中的元素型別

  8. closed 通道關閉標誌

  9. recvq 因讀取channel而陷入阻塞的協程等待佇列

  10. sendq 因傳送channel而陷入阻塞的協程等待佇列

  11. lock 鎖

waitq

// 等待佇列(雙向連結串列)
type waitq struct {
    first *sudog
    last  *sudog
}

waitq是因讀寫channel而陷入阻塞的協程等待佇列。

  1. first 佇列頭部

  2. last 佇列尾部

sudog

// sudog represents a g in a wait list, such as for sending/receiving
// on a channel.
type sudog struct {
    g *g // 等待send或recv的協程g
    next *sudog // 等待佇列下一個結點next
    prev *sudog // 等待佇列前一個結點prev
    elem unsafe.Pointer // data element (may point to stack)
    
    success bool // 標記協程g被喚醒是因為資料傳遞(true)還是channel被關閉(false)
    c        *hchan // channel
}

sudog是協程等待佇列的節點:

  1. g 因讀寫而陷入阻塞的協程

  2. next 等待佇列下一個節點

  3. prev 等待佇列前一個節點

  4. elem 對於寫channel,表示需要傳送到channel的資料指標;對於讀channel,表示需要被賦值的資料指標。

  5. success 標記協程被喚醒是因為資料傳遞(true)還是channel被關閉(false)

  6. c 指向channel的指標

1.2 通道建立

func makechan(t *chantype, size int) *hchan {
    elem := t.elem
    // buf陣列所需分配記憶體大小
    mem := elem.size*uintptr(size)
    var c *hchan
    switch {
    case mem == 0:// Unbuffered channels,buf無需記憶體分配
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
        c.buf = c.raceaddr()
    case elem.ptrdata == 0: // Buffered channels,通道元素型別非指標
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // Buffered channels,通道元素型別是指標
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)
    return c
}

通道建立主要是分配記憶體並構建hchan物件。

1.3 通道寫入

3種異常情況處理

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 1.channel為nil
    if c == nil {
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }
    
    lock(&c.lock) //加鎖
    
    // 2.如果channel已關閉,直接panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }
   
    // Block on the channel. 
    mysg := acquireSudog()
    c.sendq.enqueue(mysg) // 入sendq等待佇列
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    
    
    closed := !mysg.success // 協程被喚醒的原因是因為資料傳遞還是通道被關閉
    // 3.因channel被關閉導致阻塞寫協程被喚醒並panic
    if closed {
        panic(plainError("send on closed channel"))
    }
}

  1. 對 nil channel寫入,會死鎖

  2. 對被關閉的channel寫入,會panic

  3. 對因寫入而陷入阻塞的協程,如果channel被關閉,阻塞協程會被喚醒並panic

寫時有阻塞讀協程

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    lock(&c.lock) //加鎖
    // 1、當存在等待接收的Goroutine
    if sg := c.recvq.dequeue(); sg != nil {
        // Found a waiting receiver. We pass the value we want to send
        // directly to the receiver, bypassing the channel buffer (if any).
        send(c, sg, ep, func() { unlock(&c.lock) }, 3) // 直接把正在傳送的值傳送給等待接收的Goroutine,並將此接收協程放入可排程佇列等待排程
        return true
    }
}

// send processes a send operation on an empty channel c.
// The value ep sent by the sender is copied to the receiver sg.
// The receiver is then woken up to go on its merry way.
// Channel c must be empty and locked.  send unlocks c with unlockf.
// sg must already be dequeued from c.
// ep must be non-nil and point to the heap or the caller's stack.
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // 將ep寫入sg中的elem
    if sg.elem != nil {
         t:=c.elemtype
         dst := sg.elem
        
         // memmove copies n bytes from "from" to "to".
         memmove(dst, ep, t.size)
         sg.elem = nil // 資料已經被寫入到<- c變數,因此sg.elem指標可以置空了
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    sg.success = true
    
    // 喚醒receiver協程gp
    goready(gp, skip+1)
}

// 喚醒receiver協程gp,將其放入可執行佇列中等待排程執行
func goready(gp *g, traceskip int) {
    systemstack(func() {
        ready(gp, traceskip, true)
    })
}
// Mark gp ready to run.
func ready(gp *g, traceskip int, next bool) {
    status := readgstatus(gp)
    // Mark runnable.
    _g_ := getg()
    mp := acquirem() // disable preemption because it can be holding p in a local var
    // status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
    casgstatus(gp, _Gwaiting, _Grunnable)
    runqput(_g_.m.p.ptr(), gp, next)
    wakep()
    releasem(mp)
}
  1. 加鎖

  2. 從阻塞讀協程佇列取出sudog節點

  3. 在send方法中,呼叫memmove方法將資料複製給sudog.elem指向的變數。

  4. goready方法喚醒接收到資料的阻塞讀協程g,將其放入協程可執行佇列中等待排程

  5. 解鎖

寫時無阻塞讀協程但環形緩衝區仍有空間

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    lock(&c.lock) //加鎖
    // 當緩衝區未滿時
    if c.qcount < c.dataqsiz {
        // Space is available in the channel buffer. Enqueue the element to send.
        qp := chanbuf(c, c.sendx) // 獲取指向緩衝區陣列中位於sendx位置的元素的指標
        typedmemmove(c.elemtype, qp, ep) // 將當前傳送的值複製到緩衝區
        c.sendx++ 
        if c.sendx == c.dataqsiz {
            c.sendx = 0 // 因為是迴圈佇列,sendx等於佇列長度時置為0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }
}

  1. 加鎖

  2. 將資料放入環形緩衝區

  3. 解鎖

寫時無阻塞讀協程且環形緩衝區無空間

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    lock(&c.lock) //加鎖
    
    // Block on the channel. 
    // 將當前的Goroutine打包成一個sudog節點,並加入到阻塞寫佇列sendq裡
    gp := getg()
    mysg := acquireSudog()
    mysg.elem = ep
    mysg.g = gp
    mysg.c = c
    gp.waiting = mysg
    c.sendq.enqueue(mysg) // 入sendq等待佇列
    
   
    // 呼叫gopark將當前Goroutine設定為等待狀態並解鎖,進入休眠等待被喚醒,觸發協程排程
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    
    // 被喚醒之後執行清理工作並釋放sudog結構體
    gp.waiting = nil
    gp.activeStackChans = false
    closed := !mysg.success // gp被喚醒的原因是因為資料傳遞還是通道被關閉
    gp.param = nil
  
    mysg.c = nil
    releaseSudog(mysg)
    // 因關閉被喚醒則panic
    if closed {
        panic(plainError("send on closed channel"))
    }
    // 資料成功傳遞
    return true
}
  1. 加鎖。

  2. 將當前協程gp封裝成sudog節點,並加入channel的阻塞寫佇列sendq。

  3. 呼叫gopark將當前協程設定為等待狀態並解鎖,觸發排程其它協程執行。

  4. 因資料被讀或者channel被關閉,協程從park中被喚醒,清理sudog結構。

  5. 因channel被關閉導致協程喚醒,panic

  6. 返回

整體寫流程

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 1.channel為nil
    if c == nil {
        // 當前Goroutine阻塞掛起
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }
    // 2.加鎖
    lock(&c.lock) 
    
    // 3.如果channel已關閉,直接panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }
    // 4、存在阻塞讀協程
    if sg := c.recvq.dequeue(); sg != nil {
        // Found a waiting receiver. We pass the value we want to send
        // directly to the receiver, bypassing the channel buffer (if any).
        send(c, sg, ep, func() { unlock(&c.lock) }, 3) // 直接把正在傳送的值傳送給等待接收的Goroutine,並將此接收協程放入可排程佇列等待排程
        return true
    }
    // 5、緩衝區未滿時
    if c.qcount < c.dataqsiz {
        // Space is available in the channel buffer. Enqueue the element to send.
        qp := chanbuf(c, c.sendx) // 獲取指向緩衝區陣列中位於sendx位置的元素的指標
        typedmemmove(c.elemtype, qp, ep) // 將當前傳送的值複製到緩衝區
        c.sendx++ 
        if c.sendx == c.dataqsiz {
            c.sendx = 0 // 因為是迴圈佇列,sendx等於佇列長度時置為0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }
    // Block on the channel. 
    // 6、將當前協程打包成一個sudog結構體,並加入到channel的阻塞寫佇列sendq
    gp := getg()
    mysg := acquireSudog()
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    c.sendq.enqueue(mysg) // 入sendq等待佇列
    
    atomic.Store8(&gp.parkingOnChan, 1)
    
    // 7.呼叫gopark將當前協程設定為等待狀態並解鎖,進入休眠,等待被喚醒,並觸發協程排程
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    
    // 8. 被喚醒之後執行清理工作並釋放sudog結構體
    gp.waiting = nil
    gp.activeStackChans = false
    closed := !mysg.success // g被喚醒的原因是因為資料傳遞還是通道被關閉
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    // 9.因關閉被喚醒則panic
    if closed {
        panic(plainError("send on closed channel"))
    }
    // 10.資料成功傳遞
    return true
}


  1. channel為nil檢查。為空則死鎖。

  2. 加鎖

  3. 如果channel已關閉,直接panic。

  4. 當存在阻塞讀協程,直接把資料傳送給讀協程,喚醒並將其放入協程可執行佇列中等待排程執行。

  5. 當緩衝區未滿時,將當前傳送的資料複製到緩衝區。

  6. 當既沒有阻塞讀協程,緩衝區也沒有剩餘空間時,將協程加入阻塞寫佇列sendq。

  7. 呼叫gopark將當前協程設定為等待狀態,進入休眠等待被喚醒,觸發協程排程。

  8. 被喚醒之後執行清理工作並釋放sudog結構體

  9. 喚醒之後檢查,因channel被關閉導致協程喚醒則panic。

  10. 返回。

1.4 通道讀

2種異常情況處理

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 1.channel為nil
    if c == nil {
        // 否則,當前Goroutine阻塞掛起
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    lock(&c.lock)
    // 2.如果channel已關閉,並且緩衝區無元素,返回(true,false)
    if c.closed != 0 {
        if c.qcount == 0 {
            unlock(&c.lock)
            if ep != nil {
                //根據channel元素的型別清理ep對應地址的記憶體,即ep接收了channel元素型別的零值
                typedmemclr(c.elemtype, ep)
            }
            return true, false
        }
    }
}
  1. channel未初始化,讀操作會死鎖

  2. channel已關閉且緩衝區無資料,給讀變數賦零值。

讀時有阻塞寫協程

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    lock(&c.lock)
    
    // Just found waiting sender with not closed.
    // 等待傳送的佇列sendq裡存在Goroutine
    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).
        // 如果無緩衝區,那麼直接從sender接收資料;否則,從buf佇列的頭部接收資料,並把sender的資料加到buf佇列的尾部
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true // 接收成功
    }
    
    
}

// recv processes a receive operation on a full channel c.
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // channel無緩衝區,直接從sender讀
    if c.dataqsiz == 0 {
        if ep != nil {
            // copy data from sender
            t := c.elemtype
            src := sg.elem
            typeBitsBulkBarrier(t, uintptr(ep), uintptr(src), t.size)
            memmove(dst, src, t.size)
        }
    } else {
        // 從佇列讀,sender再寫入佇列
        qp := chanbuf(c, c.recvx)
        // copy data from queue to receiver
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        // copy data from sender to queue
        typedmemmove(c.elemtype, qp, sg.elem)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
    }
    // 喚醒sender佇列協程sg
    sg.elem = nil
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    sg.success = true
    // 喚醒協程
    goready(gp, skip+1)
}
  1. 加鎖

  2. 從阻塞寫佇列取出sudog節點

  3. 假如channel為無緩衝區通道,則直接讀取sudog對應寫協程資料,喚醒寫協程。

  4. 假如channel為緩衝區通道,從channel緩衝區頭部(recvx)讀資料,將sudog對應寫協程資料,寫入緩衝區尾部(sendx),喚醒寫協程。

  5. 解鎖

讀時無阻塞寫協程且緩衝區有資料

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    lock(&c.lock)
    // 緩衝區buf中有元素,直接從buf複製元素到當前協程(在已關閉的情況下,佇列有資料依然會讀)
    if c.qcount > 0 {
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)// 將從buf中取出的元素複製到當前協程
        }
        typedmemclr(c.elemtype, qp) // 同時將取出的資料所在的記憶體清空
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true // 接收成功
    }
}
  1. 加鎖

  2. 從環形緩衝區讀資料。在channel已關閉的情況下,緩衝區有資料依然可以被讀。

  3. 解鎖

讀時無阻塞寫協程且緩衝區無資料

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    lock(&c.lock)

    // no sender available: block on this channel.
    // 阻塞模式,獲取當前Goroutine,打包一個sudog,並加入到channel的接收佇列recvq裡
    gp := getg()
    mysg := acquireSudog()
    mysg.elem = ep
    gp.waiting = mysg
    mysg.g = gp
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg) // 入接收佇列recvq
    
    // 掛起當前Goroutine,設定為_Gwaiting狀態,進入休眠等待被喚醒
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // 因通道關閉或者讀到資料被喚醒
    gp.waiting = nil
    success := mysg.success
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true, success // 10.返回成功
}
  1. 加鎖。

  2. 將當前協程gp封裝成sudog節點,加入channel的阻塞讀佇列recvq。

  3. 呼叫gopark將當前協程設定為等待狀態並解鎖,觸發排程其它協程執行。

  4. 因讀到資料或者channel被關閉,協程從park中被喚醒,清理sudog結構。

  5. 返回

整體讀流程

// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 1.channel為nil
    if c == nil {
        // 否則,當前Goroutine阻塞掛起
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }
    // 2.加鎖
    lock(&c.lock)
    // 3.如果channel已關閉,並且緩衝區無元素,返回(true,false)
    if c.closed != 0 {
        if c.qcount == 0 {
            unlock(&c.lock)
            if ep != nil {
                //根據channel元素的型別清理ep對應地址的記憶體,即ep接收了channel元素型別的零值
                typedmemclr(c.elemtype, ep)
            }
            return true, false
        }
        // The channel has been closed, but the channel's buffer have data.
    } else {
        // Just found waiting sender with not closed.
        // 4.存在阻塞寫協程
        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).
            // 如果無緩衝區,那麼直接從sender接收資料;否則,從buf佇列的頭部接收資料,並把sender的資料加到buf佇列的尾部
            recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
            return true, true // 接收成功
        }
    }
    // 5.緩衝區buf中有元素,直接從buf複製元素到當前協程(在已關閉的情況下,佇列有資料依然會讀)
    if c.qcount > 0 {
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)// 將從buf中取出的元素複製到當前協程
        }
        typedmemclr(c.elemtype, qp) // 同時將取出的資料所在的記憶體清空
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true // 接收成功
    }

    // no sender available: block on this channel.
    // 6.獲取當前Goroutine,封裝成sudog節點,加入channel阻塞讀佇列recvq
    gp := getg()
    mysg := acquireSudog()
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg) // 入接收佇列recvq
    
    atomic.Store8(&gp.parkingOnChan, 1)
    // 7.掛起當前Goroutine,設定為_Gwaiting狀態,進入休眠等待被喚醒
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // 8.因通道關閉或者可讀被喚醒
    gp.waiting = nil
    gp.activeStackChans = false
    success := mysg.success
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    // 9.返回
    return true, success 
}


通道讀流程如下:

  1. channel為nil檢查。空則死鎖。

  2. 加鎖。

  3. 如果channel已關閉,並且緩衝區無資料,讀變數賦零值,返回。

  4. 當存在阻塞寫協程,如果緩衝區已滿,則直接從sender接收資料;否則,從環形緩衝區頭部接收資料,並把sender的資料加到環形緩衝區尾部。喚醒sender,將其放入協程可執行佇列中等待排程執行,返回。

  5. 如果緩衝區中有資料,直接從緩衝區複製資料到當前協程,返回。

  6. 當既沒有阻塞寫協程,緩衝區也沒有資料時,將協程加入阻塞讀佇列recvq。

  7. 呼叫gopark將當前協程設定為等待狀態,進入休眠等待被喚醒,觸發協程排程。

  8. 因通道關閉或者可讀被喚醒。

  9. 返回。

1.5 通道關閉

func closechan(c *hchan) {
    // // 1.channel為nil則panic
    if c == nil {
        panic(plainError("close of nil channel"))
    }
    lock(&c.lock)
    // 2.已關閉的channel再次關閉則panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }
    // 設定關閉標記
    c.closed = 1

    var glist gList
    // 遍歷recvq和sendq中的協程放入glist
    // release all readers
    for {
        sg := c.recvq.dequeue()
        if sg == nil {
            break
        }
        if sg.elem != nil {
            typedmemclr(c.elemtype, sg.elem)
            sg.elem = nil
        }
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = unsafe.Pointer(sg)
        sg.success = false
        glist.push(gp)
    }

    // release all writers (they will panic)
    for {
        sg := c.sendq.dequeue()
        if sg == nil {
            break
        }
        sg.elem = nil
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = unsafe.Pointer(sg)
        sg.success = false
        glist.push(gp)
    }
    unlock(&c.lock)

    // 3.將glist中所有Goroutine的狀態置為_Grunnable,等待排程器進行排程
    for !glist.empty() {
        gp := glist.pop()
        gp.schedlink = 0
        goready(gp, 3)
    }
}

  1. channel為nil檢查。為空則panic

  2. 已關閉channel再次被關閉,panic

  3. 將sendq和recvq所有Goroutine的狀態置為_Grunnable,放入協程排程佇列等待排程器排程

2 高頻面試題

  1. channel 的底層實現原理 (資料結構)

  2. nil、關閉的 channel、有資料的 channel,再進行讀、寫、關閉會怎麼樣?(各類變種題型)

  3. 有緩衝channel和無緩衝channel的區別

原文連結https://reurl.cc/Wx26jD

相關文章