Go channel 读流程

48 阅读5分钟

接下来看看channel的读过程look:

// c channel内部指针结构
// eq 接受数据存储的地址
// block 是否阻塞接收
// selected 是否成功从channel中读取到数据
// received channel是否关闭
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")
    }

    // 判断cahnnel是否为nil 如果是nil的话 直接死锁 无法被唤醒
    // 阻塞当前channel goroutine将不会被唤醒
    if c == nil {
       if !block {
          return
       }
       gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
       throw("unreachable")
    }

    // Fast path: check for failed non-blocking operation without acquiring the lock.
    // 非阻塞 channel为空
    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.
      
      // 如果channel关闭且为空 返回零值 接收失败
       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 {
        // 缓冲区卫空 直接返回 
       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 {
       // Just found waiting sender with not closed.
       // 这里通道没有关闭 取等待的发送的goroutine,就是写的时候等待被唤醒的goroutine
       // 从阻塞的写协程队列中获取一个写goroutine
       // 如果channel无缓冲区,直接读取写协程的元素,唤醒写goroutine
       // 如果channel有缓冲区,直接读取缓冲区头部元素,并将写goroutine元素写入缓冲区尾部并唤醒写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).
          recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
          return true, true
       }
    }

    // 缓冲区中有数据 读取缓冲区数据
    if c.qcount > 0 {
       // Receive directly from queue
       // 知道recvx的值 直接从buf中拿到相对应的数据就可以
       qp := chanbuf(c, c.recvx)
       if raceenabled {
          racenotify(c, c.recvx, nil)
       }
       if ep != nil {
          typedmemmove(c.elemtype, ep, qp)
       }
       // 拷贝数据
       typedmemclr(c.elemtype, qp)
       // 拿到数据之后自增recvx++
       c.recvx++
       // recvx == 当前channel的大小 直接 recvx == 0 形成一个闭环 
       // 就是变成一个环形数组
       if c.recvx == c.dataqsiz {
          c.recvx = 0
       }
       // 数量-1 解锁返回
       c.qcount--
       unlock(&c.lock)
       return true, true
    }

    // 非阻塞模式下 channel 为空 直接返回
    if !block {
       unlock(&c.lock)
       return false, false
    }

    // no sender available: block on this channel.
    // 阻塞模式下等待接收数据 缓冲区没有元素可以读
    // 构造sudog对象 完成指针指向操作
    // 把封装好的sudog添加(挂起)到当前channel的阻塞读协程队列中 
    // 等待被唤醒
    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.
    gp.parkingOnChan.Store(true)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, ,,traceEvGoBlockRecv, 2)

    // someone woke us up
    // 如果被唤醒 则回收sudog 恢复goroutine状态
    // 这里被唤醒其实就是有对应的元素写入
    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
}

总结:还是一样为初始化的channel读取的时候会死锁,挂起永远不会被唤醒。判断如果非阻塞channel为空,channel未关闭,直接返回,然后判断channel为读取成功,通道关闭。
接着上锁,如果当前通道已经关闭,缓冲区的已经没有数据直接释放锁,然后返回读取成功,通道已经关闭。如果通道不关闭,去写的snedq队列中去取等待被唤醒的goroutine队列去执行。如果是有缓冲区的channel则会去取头部的数据,并去唤醒正在等待唤醒的写协程执行将数据插入尾部,如果是无缓冲,直接取等待唤醒的写协程队列的数据并且唤醒写协程。
如果是有缓冲区并且缓冲区中有数据,先去拿到recvxd去获取到缓冲区的数据,然后进行recvx++然后判断recvx和datasiz是否相等,如果想等的话recvx = 0 形成一个闭环,就是环形数组。然后进行qcount--,释放锁,返回。 如果在阻塞缓冲区没有数据可以读的情况下,这个时候就要构造sudog,完成指针指向操作,封装完成sudog的然后挂起加入recvxq的队列中,等待唤醒。

这里其实就比较明确了。如果是读的时候发生阻塞,就去唤醒写的goroutine。如果是写的时候阻塞就去唤醒读的goroutine。

我这样理解不知道对不对,有错的地方希望指出,这样才有进步。