Go channel(二)|Go主题月

562 阅读2分钟

先是纯粹的源码解读,最后会串起来。

recv

// 1
v, ok := <- ch
// 2 
v := <- ch

以上两种方式都会走一个函数:chanrecv(ch, elem, true)

// entry points for <- c from compiled code
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}
// received 就是返回的 ok
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
	_, received = chanrecv(c, elem, true)
	return
}

两个函数传入的 block 都是 true,都是阻塞方式,分析的时候不考虑 block=false 的情况。

第 I 部分

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// debug code
  ...
	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
}

chan 为 nil,和 send 一样,从 nil chan 里面读取数据,调用者会被永远阻塞【不太可能会执行的地方】

第 II 部分

skip。!block 不在我们这次的讨论范畴内【我也看不到这在哪出现】

第 III 部分

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  ...
  // 加锁,返回时释放锁【全局锁】
	lock(&c.lock)
  // chan 被close && chan为空
	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}
  ...
}

很清楚啊。因为现在终于开始要操作这个 buf ,加锁,而且是一个全局锁,此时要么只能 send/recv

此时 chan close && chan empty ,这个 chan 啥都没了,还能干什么。直接返回 false

第 IV 部分

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  ...
  if sg := c.sendq.dequeue(); sg != nil {
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}
  ...
}

等待发送队列有 G ,说明 buf is full

  • unbuff

    首先 chan 是不能存储数据,现在既然 G 需要接收数据,而且有发送 G,直接就是 send G -> recv G

  • buff

    buf 头部取一个元素直接拷贝给 recv 接受地址;同时将出队这个 sender 的值插到 buf 尾部【这里有空位,最直接的办法就是把空填上】

第 V 部分

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  ...
  if c.qcount > 0 {
		// 直接从循环数组里取一个当前的元素【recvx表示recv在buf中的index】
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
    // ep就是接收端拿到的数据,如果 <- ch,这里就直接忽略;如果是 val <- ch,ep指向val
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
    // 移除取走的值
		typedmemclr(c.elemtype, qp)
    // 往后推移
		c.recvx++
    // recv 的位置已经到buf的尾部,说明
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
    // buf 数组里的元素个数减 1
		c.qcount--
		unlock(&c.lock)
		return true, true
	}

	if !block {
		unlock(&c.lock)
		return false, false
	}
  ...
}
  • c.qcount > 0 说明 buf 还有数据,针对的是 buf chan
  • 从上一部分下来,说明 没有等待的 sender
  • buf 中取一个元素准备返回,并将该元素在 buf 中删除,recvx++, qcount--
  • 解锁【这个解锁是全局锁,和 chansend 公用,这也确保只有只有一个操作在 buf 上】

第 VI 部分

到这一步了,情况就变成这样了:

  • buf 没有数据
  • sender 发送 G 也没有

那就是说只有当前这个 recverG ,那就直接被阻塞:

  1. 构造一个 sudog

  2. 待接收数据的地址保存下来

  3. 加入 chan 的等待接收队列

  4. 当前的 G 挂起【gopack

  5. 后续就是 G 被唤醒后的各种收尾工作了【释放,更改标志啥的】

具体代码就不展示了。

总结

整个 recv 过程分为几个阶段:

  • unbuf chan【本身是不能缓存数据的】
    • 有发送 G,直接拷贝数据,完事;
    • 没有发送 G,阻塞当前 G ,入队接收队列,挂起;
  • buf chan
    • buf 有数据:从 buf 中处于 recx 位置数据拷贝出去给接收者【分有无阻塞的发送者,区别只在发送者的数据问题上】
    • buf 没数据:阻塞当前 recv G,入队接收队列,挂起;【没有发送者的情况下,如果有就和发送者之前进行数据拷贝】