必知必会系列-Channel

184 阅读12分钟

设计思想

不要通过共享内存来通信,而是通过通信来实现共享内存

Do not communicate by sharing memory; instead, share memory by communicating

代码映射到源码的实现

  • 初始化channel
// make(chan int) or make(chan int, 10)
// size!=0 初始化带缓冲区的通道
// size=0 初始化不带缓冲区的通道
makechan(t *chantype, size int) *hchan
  • 向channel中发送数据
// c <- x 
func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool ,  callerpc uintptr) bool {}
  • 从channel中读取数据
// <- c 
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

// x, ok:= <- c
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

// block = false 非阻塞请求 用于select语句中
// block = true  阻塞式请求 其他场景的发送和接收都是阻塞式的 
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
```go

-   **关闭channel**

```go
// close(c)
func closechan(c *hchan) {}
  • 在select中使用channel
// compiler implements
//
//  select {
//  case c <- v:
//      ... foo
//  default:
//      ... bar
//  }
//
// as
//
//  if selectnbsend(c, v) {
//      ... foo
//  } else {
//      ... bar
//  }
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
    return chansend(c, elem, false, getcallerpc())
}

// compiler implements
//
//  select {
//  case v, ok = <-c:
//      ... foo
//  default:
//      ... bar
//  }
//
// as
//
//  if selected, ok = selectnbrecv(&v, c); selected {
//      ... foo
//  } else {
//      ... bar
//  }
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
    return chanrecv(c, elem, false)
}

接下里的篇幅 从channel的底层构成逐步分析

  • channel的初始化
  • channel数据写入
  • channel 数据读取
  • channel 关闭

channel 底层结构体

hchan

channel的底层结构

  • qcount:channel中已存放元素个数
  • dataqsize: channel 能存放的元素容量
  • buf:channel 中用于存放元素的环形缓冲区
  • elemsize:channel 元素类型的大小
  • closed:标识 channel 是否关闭
  • elemtype:channel 元素类型
  • sendx:环形缓冲区可生产下标
  • recvx:环形缓冲区可消费下标
  • recvq:接收者阻塞队列(比如channel空)
  • sendq:发送者阻塞队列(比如channel满)
type hchan struct {
    qcount   uint           // 目前环形缓冲区存在元素个数
    dataqsiz uint           // 环形缓冲区长度
    buf      unsafe.Pointer // 长度为dataqsiz 的数组
    elemsize uint16  // channel元素 占据的大小
    closed   uint32  // 标识着channel是否关闭
    elemtype *_type  // element type channel元素类型
    sendx    uint    // send index
    recvx    uint    // receive index
    recvq    waitq   // list of recv waiters 接受者等待队列
    sendq    waitq   // list of send waiters 发送者等待队列

    // lock protects all fields in hchan, as well as several
    // fields in sudogs blocked on this channel.
    //
    // Do not change another G's status while holding this lock
    // (in particular, do not ready a G), as this can deadlock
    // with stack shrinking.
    lock mutex
}

sudog

包装正在阻塞状态的G(协程)

  • g:与之关联的协程
  • next:阻塞队列中的下一个节点
  • prev:阻塞队列中的上一个节点
  • isSelect:当前协程是否处在 select 语句中
  • go: 阻塞相关的channel
  • success false: 因为通道关闭而唤醒 true: 正常唤醒
// sudog represents a g in a wait list, such as for sending/receiving
// on a channel.
//
// sudog is necessary because the g ↔ synchronization object relation
// is many-to-many. A g can be on many wait lists, so there may be
// many sudogs for one g; and many gs may be waiting on the same
// synchronization object, so there may be many sudogs for one object.
//
// sudogs are allocated from a special pool. Use acquireSudog and
// releaseSudog to allocate and free them.
type sudog struct {
    // The following fields are protected by the hchan.lock of the
    // channel this sudog is blocking on. shrinkstack depends on
    // this for sudogs involved in channel ops.

    g *g

    next *sudog
    prev *sudog
    elem unsafe.Pointer // data element (may point to stack)

    // The following fields are never accessed concurrently.
    // For channels, waitlink is only accessed by g.
    // For semaphores, all fields (including the ones above)
    // are only accessed when holding a semaRoot lock.

    acquiretime int64
    releasetime int64
    ticket      uint32

    // isSelect indicates g is participating in a select, so
    // g.selectDone must be CAS'd to win the wake-up race.
    isSelect bool

    // success indicates whether communication over channel c
    // succeeded. It is true if the goroutine was awoken because a
    // value was delivered over channel c, and false if awoken
    // because c was closed.
    success bool

    parent   *sudog // semaRoot binary tree
    waitlink *sudog // g.waiting list or semaRoot
    waittail *sudog // semaRoot
    c        *hchan // channel
}

waitq

协程的阻塞队列

  • • first:队头
  • • last:队尾
type waitq struct {
    first *sudog
    last  *sudog
}

初始化channel

channel初始化分为三种:

  1. 无缓冲型仅需要申请hchanSize(98)大小的内存大小即可
  2. 有缓冲(元素不包含指针)一次性分配好 96 + mem 大小的连续内存,并且调整 chan 的 buf 指向 mem 的起始位置`(这种情况下该结构体无需GC)
  3. 有缓冲(元素包含指针) 分别申请 hchanSize 和 buf所需空间
const (
    maxAlign  = 8
    hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
    debugChan = false
)

func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

     // Hchan does not contain pointers interesting for GC 
  // when elements stored in buf do not contain pointers. 
    // buf points into the same allocation, elemtype is persistent.
    // SudoG's are referenced from their owning thread 
    // so they can't be collected.
    // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
    var c *hchan
    switch {
    case mem == 0:
        // Queue or element size is zero.
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // Elements do not contain pointers.
        // Allocate hchan and buf in one call.
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // Elements contain pointers.
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)
    lockInit(&c.lock, lockRankHchan)

    if debugChan {
        print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
    }
    return c
}

向channel写入数据

  • 向未初始化的通道中写入元素 会一直阻塞

  • 加锁,分情况处理

    • 接收阻塞队列recvq不为空

      • 直接从阻塞队列recvq取出一个sudog
      • 直接将待发送的数据拷贝到对应的sudog下elem
      • 唤醒sudog中的G
    • 阻塞队列recvq为空 && 缓冲区未满

      • 该待发送数据 放入缓冲区中
    • 阻塞队列recvq为空 && 缓冲区已满

      • 则将当前G包装成sudog(待发送的数据放在sudo.elem),加入发送阻塞队列sendq
      • 调用gopark 该协程进入阻塞态
      • 被其他协程唤醒(说明sudo.elem暂存的数据已经被读取),回收 sudog
  • 释放锁

// entry point for c <- x from compiled code
//
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc())
}


func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    if c == nil {
        if !block {
            return false
        }
 // 向未初始化的通道中写入元素 会一直阻塞
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    if debugChan {
        print("chansend: chan=", c, "\n")
    }

    if raceenabled {
        racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
    }

    // Fast path: check for failed non-blocking operation without acquiring the lock.
    //
    // After observing that the channel is not closed, we observe that the channel is
    // not ready for sending. Each of these observations is a single word-sized read
    // (first c.closed and second full()).
    // Because a closed channel cannot transition from 'ready for sending' to
    // 'not ready for sending', even if the channel is closed between the two observations,
    // they imply a moment between the two when the channel was both not yet closed
    // and not ready for sending. We behave as if we observed the channel at that moment,
    // and report that the send cannot proceed.
    //
    // It is okay if the reads are reordered here: if we observe that the channel is not
    // ready for sending and then observe that it is not closed, that implies that the
    // channel wasn't closed during the first observation. However, nothing here
    // guarantees forward progress. We rely on the side effects of lock release in
    // chanrecv() and closechan() to update this thread's view of c.closed and full().
    if !block && c.closed == 0 && full(c) {
        return false
    }

    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
    }

    lock(&c.lock)

    if c.closed != 0 {
        unlock(&c.lock)
        panic (plainError(  "send on closed channel"  )) 
    }

    if sg := c.recvq.dequeue(); sg != nil {
        // 如果接收阻塞队列不为空 则直接从阻塞队列中唤醒一个sudog
        // 直接将待发送的数据拷贝到对应的sudog下elem
        // 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 ) 
        return true
    }

    // 如果缓冲区未满 则将该待发送数据 放入缓冲区中
    if c.qcount < c.dataqsiz {
        // Space is available in the channel buffer. Enqueue the element to send.
        qp := chanbuf(c, c.sendx)
        if raceenabled {
            racenotify(c, c.sendx, nil)
        }
        typedmemmove(c.elemtype, qp, ep)
        c.sendx++
        if c.sendx == c.dataqsiz {
            c.sendx = 0
        }
        c.qcount++
        unlock(&c.lock)
        return true
    }

    if !block {
        unlock(&c.lock)
        return false
    }

    // 缓冲区满 这时候只能阻塞了
    // Block on the channel. Some receiver will complete our operation for us.
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    // 加入发送阻塞队列
    c.sendq.enqueue(mysg)
    // Signal to anyone trying to shrink our stack that we're about
    // to park on a channel. The window between when this G's status
    // changes and when we set gp.activeStackChans is not safe for
    // stack shrinking.
    atomic.Store8(&gp.parkingOnChan, 1)
    // 该协程进入阻塞态
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    // Ensure the value being sent is kept alive until the
    // receiver copies it out. The sudog has a pointer to the
    // stack object, but sudogs aren't considered as roots of the
    // stack tracer.
    KeepAlive(ep)

    // someone woke us up.
    if mysg != gp.waiting {
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    gp.activeStackChans = false
    closed := !mysg.success
    gp.param = nil
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    mysg.c = nil
    releaseSudog(mysg)
    if closed {
        if c.closed == 0 {
            throw("chansend: spurious wakeup")
        }
        // 通道已经关闭了 写入流程panic !!!
        panic(plainError("send on closed channel"))
    }
    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) {
    if raceenabled {
        // 省略 ...
    }
    if sg.elem != nil {
        // 将待send的数据 拷贝到 对应的sudog下的elem
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
 sg.success = true
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    // 唤醒 sudog
    goready(gp, skip+1)
}

从channel中读取数据

  • channel未初始化 则一直阻塞

  • channel 已关闭且环形缓冲区无数据,则直接返回默认数据

  • 加锁,根据不同的情况处理

    • 有阻塞的写协程(即sendq不为空)

      • sendq中获取到一个写协程
      • 如果 channel 无缓冲区,则直接读取写协程元素,并唤醒写协程
      • 如果 channel 有缓冲区,则读取缓冲区头部元素,并将写协程元素写入缓冲区尾部后唤醒写协程
    • 无阻塞写协程 && 缓冲区中有数据

      • 拷贝 recvx 对应位置的元素
    • 无阻塞写协程 && 缓存区无数据

      • 包装 sudog,将其加入recvq,调用gopark阻塞该协程
      • 被其他协程唤醒(说明elem已经被其他协程写入数据),回收 sudog
  • 解锁

// x <- c 
//
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

// x, ok := <- c
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

读取数据 本质上都会调用chanrecv

// 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) {
    // raceenabled: don't need to check ep, as it is always on the stack
    // or is new memory allocated by reflect.

    if debugChan {
        print("chanrecv: chan=", c, "\n")
    }

    if c == nil {
        if !block {
            return
        }
        // channel未初始化 --> 一直阻塞
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    // Fast path: check for failed non-blocking operation without acquiring the lock.
    if !block && empty(c) {
        // After observing that the channel is not ready for receiving, we observe whether the
        // channel is closed.
        //
        // Reordering of these checks could lead to incorrect behavior when racing with a close.
        // For example, if the channel was open and not empty, was closed, and then drained,
        // reordered reads could incorrectly indicate "open and empty". To prevent reordering,
        // we use atomic loads for both checks, and rely on emptying and closing to happen in
        // separate critical sections under the same lock.  This assumption fails when closing
        // an unbuffered channel with a blocked send, but that is an error condition anyway.
        if atomic.Load(&c.closed) == 0 {
            // Because a channel cannot be reopened, the later observation of the channel
            // being not closed implies that it was also not closed at the moment of the
            // first observation. We behave as if we observed the channel at that moment
            // and report that the receive cannot proceed.
            return
        }
        // The channel is irreversibly closed. Re-check whether the channel has any pending data
        // to receive, which could have arrived between the empty and closed checks above.
        // Sequential consistency is also required here, when racing with such a send.
        if empty(c) {
            // The channel is irreversibly closed and empty.
            if raceenabled {
                raceacquire(c.raceaddr())
            }
            if ep != nil {
                typedmemclr(c.elemtype, ep)
            }
            return true, false
        }
    }

    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
    }

    lock(&c.lock)

    if c.closed != 0 {
        // channel 已关闭且环形缓冲区无数据 --> 直接返回默认数据
        if c.qcount == 0 {
            if raceenabled {
                raceacquire(c.raceaddr())
            }
            unlock(&c.lock)
            if ep != nil {
                typedmemclr(c.elemtype, ep)
            }
            return true, false
        }
        // The channel has been closed, but the channel's buffer have data.
    } else {
        // 有阻塞的写协程(即sendq不为空) --> 取出一个阻塞的协程处理(逻辑见revc)
        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
        }
    }

    // 无阻塞写协程 && 缓冲区中有数据  --> 直接从缓冲区中拷贝数据
    if c.qcount > 0 {
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            racenotify(c, c.recvx, nil)
        }
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        typedmemclr(c.elemtype, qp)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

    if !block {
        unlock(&c.lock)
        return false, false
    }

    // 无阻塞写协程 && 缓存区无数据  --> 等待其他协程唤醒
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }
    // No stack splits between assigning elem and enqueuing mysg
    // on gp.waiting where copystack can find it.
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    c.recvq.enqueue(mysg)
    // Signal to anyone trying to shrink our stack that we're about
    // to park on a channel. The window between when this G's status
    // changes and when we set gp.activeStackChans is not safe for
    // stack shrinking.
    atomic.Store8(&gp.parkingOnChan, 1)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // someone woke us up
    if mysg != gp.waiting {
        throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    gp.activeStackChans = false
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    success := mysg.success
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true, success
}

recv

  • sendq中获取到一个阻塞的写协程
  • 如果 channel 无缓冲区,则直接读取写协程元素,并唤醒写协程
  • 如果 channel 有缓冲区(sendq不为空,说明环形缓冲区满),则读取缓冲区头部元素,并将写协程元素写入缓冲区尾部后唤醒写协程
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if c.dataqsiz == 0 {
        if raceenabled {
            racesync(c, sg)
        }
        if ep != nil {
            // copy data from sender
            // 直接从 sudog.elem读取数据
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
        // Queue is full. Take the item at the
        // head of the queue. Make the sender enqueue
        // its item at the tail of the queue. Since the
        // queue is full, those are both the same slot.
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            racenotify(c, c.recvx, nil)
            racenotify(c, c.recvx, sg)
        }
        // 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
    }
    sg.elem = nil
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    sg.success = true
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

关闭channel

  • 加锁将标志位 close设置为1(重复close会panic)
  • 收集阻塞队列recvqsendq(向已关闭channel中写入数据会panic)中的所有sudog
  • 释放锁
  • 唤醒所有的阻塞的G
func closechan(c *hchan) {
    if c == nil {
        panic(plainError("close of nil channel"))
    }

    lock(&c.lock)
    // 这里也解释了为什么 重复关闭chnnel会panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

    if raceenabled {
        callerpc := getcallerpc()
        racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
        racerelease(c.raceaddr())
    }

    c.closed = 1

    var glist gList

     // 收集所有因为接收数据阻塞的sudog
    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
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }

     // 收集所有因为发送数据阻塞的sudog(已关闭的通道写入数据会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
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }
    unlock(&c.lock)

    // Ready all Gs now that we've dropped the channel lock.
    for !glist.empty() {
        gp := glist.pop()
        gp.schedlink = 0
        // 唤醒对应的 G
        goready(gp, 3)
    }
}