先是纯粹的源码解读,最后会串起来。
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也没有
那就是说只有当前这个 recver 的 G ,那就直接被阻塞:
-
构造一个
sudog -
待接收数据的地址保存下来
-
加入
chan的等待接收队列 -
当前的
G挂起【gopack】 -
后续就是
G被唤醒后的各种收尾工作了【释放,更改标志啥的】
具体代码就不展示了。
总结
整个 recv 过程分为几个阶段:
unbuf chan【本身是不能缓存数据的】- 有发送
G,直接拷贝数据,完事; - 没有发送
G,阻塞当前G,入队接收队列,挂起;
- 有发送
buf chan:buf 有数据:从buf中处于recx位置数据拷贝出去给接收者【分有无阻塞的发送者,区别只在发送者的数据问题上】buf 没数据:阻塞当前recv G,入队接收队列,挂起;【没有发送者的情况下,如果有就和发送者之前进行数据拷贝】