目录
在Golang中,chan是我们常用的数据结构,chan是并发安全的,在使用它的时候我们无需考虑锁的问题。而且chan使用非常方便,chan与goroutine配合,很容易就可以实现一个生产者/消费者模型。了解chan的源码可以让我们了解更多关于chan的实现以及更好的使用。
在介绍chan之前,我们可以想一下chan的基本结构,首先,从chan的使用上我们应该就会猜到它里面应该是一个队列的结构,并且他采取了一定的措施来保证并发安全。其次,在读取或写入chan的时候,协程可能会阻塞,因此里面也需要有阻塞队列来保存阻塞的goroutine的g结构。
1、chan数据结构的定义
chan的实现在runtime
包的chan.go
中,chan的结构体定义如下:
type hchan struct {
qcount uint // 队列中的总数据数量,也就是队列中当前存放的元素的个数
dataqsiz uint // 循环队列的大小,chan的大小
buf unsafe.Pointer // 指向数组的指针,逻辑上组成一个环形队列
elemsize uint16 // 存放的元素类型的大小,比如int是8个字节
closed uint32 // chan是否被关闭的标志
elemtype *_type // 元素类型
sendx uint // send index 发送的索引
recvx uint // receive index 接收的索引
recvq waitq // list of recv waiters 等待接收数据的goroutine阻塞队列
sendq waitq // list of send waiters 等待发送数据的goroutine阻塞队列
// lock用来保护hchan中的所有字段
lock mutex
}
// sudog是对g的一个包装,其中包含了当前g,以及next和prev指针,可以组成一个双向链表
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // 可以使用这个指针来直接在g之间传递数据
... // 省略了不太关注的字段
}
// 等待队列,waitq 是 sudog 的一个双向链表
type waitq struct {
first *sudog
last *sudog
}
有缓冲的chan int32 的结构如下图所示:
为了方便画图,将sendx放在了recvx的下面。可以看到chan中是有一个互斥锁的,chan并不是无锁队列。那么为什么它的效率还比较高呢,是因为只在传递数据时进行加锁,锁的粒度小,因此效率比较高。
2、创建chan
我们可以通过将代码生成汇编来查看创建chan底层调用的函数。将下面代码生成汇编代码可以看到在底层创建chan是调用了runtime.makechan函数。
package main
import "fmt"
func main() {
channel := make(chan int, 5)
// 为了不报错,打印一下
fmt.Println(channel)
}
可以使用 go tool compile -N -l -S main.go 或 go build -gcflags -S main.go来将源代码编译为汇编代码。
makechan的代码如下:
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 检查要创建的管道的元素的大小
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
// 检查内存对齐
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
// 判断要申请的空间大小是否越界, mem为相乘后的结果,overflow为是否越界,bool类型
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
var c *hchan
switch {
case mem == 0:
// make(chan type) 创建无缓冲的chan,此处为hchan结构分配空间
c = (*hchan)(mallocgc(hchanSize, nil, true))
...
case elem.ptrdata == 0:
// 如果元素不包含指针,一次同时申请hchan和hchan.buf的内存
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素包含指针,分别申请内存
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 记录元素大小
c.elemsize = uint16(elem.size)
// 记录元素类型
c.elemtype = elem
// 循环队列的大小,也就是可容纳的元素的个数,chan的大小
c.dataqsiz = uint(size)
// 初始化互斥锁
lockInit(&c.lock, lockRankHchan)
...
return c
}
在makechan函数中,主要是对hchan结构进行赋值,并返回该结构体指针。
3、发送数据 c <-
向管道中发送数据,通过汇编看到调用了runtime.chansend1函数:
chan发送数据有三种情形:
直接发送:
发送时,环形队列中没有数据,而且有接收者正在读数据,直接将数据交给接收者。放入缓冲区:
有缓冲的chan,而且缓冲区有空闲空间,直接将数据放入缓冲区中。休眠等待:
无缓冲的chan或者缓冲区满了,而且没有接收者正在接受数据,发送者休眠等待。
3.1 直接发送
直接发送的情形如下,此时环形队列中没有数据,或者是无缓冲的chan。有goroutine正在接受队列中,出队一个G,然后直接将数据交给出队的G,然后将G唤醒,数据不会经过缓冲区。直接通过sudog中的elem进行数据传递(elem指针指向的是用户接受变量的地址,将数据拷贝到elem指向的地址,即可接收数据,比如对于 d <- c 操作,elem指向的就是d的地址
)。
代码如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 出队一个g,如果sg != nil,说明,环形缓冲区中肯定没有数据(不然也不会有g在接收队列中了),或者是无缓冲的chan
if sg := c.recvq.dequeue(); sg != nil {
// 直接将数据传递给出队的g
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
...
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
...
// 直接将数据传递过去
if sg.elem != nil {
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()
}
// 唤醒出队的协程
goready(gp, skip+1)
}
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
...
// 使用memmove将数据拷贝过去
memmove(dst, src, t.size)
}
3.2 放入缓冲区
放入缓冲区的情形如下:此时没有正在接收的goroutine,而且缓冲区有空闲空间,sender直接将数据放入环形缓冲区中。
代码如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 这个条件不成立
if sg := c.recvq.dequeue(); sg != nil {
...
}
// 判断缓冲区是否满了,在此处没有满
if c.qcount < c.dataqsiz {
// 获取chan中的缓冲区下一个可发送的空闲位置
qp := chanbuf(c, c.sendx)
...
// 将数据拷贝到缓冲区中
typedmemmove(c.elemtype, qp, ep)
// sendx 也就是下一个可发送的位置索引+1
c.sendx++
// 环形队列,sendx == dataqsiz时,重置为0
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 缓冲区中元素数量+1
c.qcount++
unlock(&c.lock)
return true
}
...
}
3.3 休眠等待
休眠等待的情形如下:此时缓冲区满了或者没有缓冲区(无缓冲chan),可能还有其它的sender正在等待发送数据。由于没有goroutine正在接收数据而且缓冲区没有空闲位置,因此当前发送的goroutine会进入chan的发送队列中休眠。休眠结束后,不用再将数据发送给接收的goroutine或者放入缓冲中,因为在被唤醒前,接收goroutine已经将数据取走了。
代码如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 发送队列中没有goroutine
if sg := c.recvq.dequeue(); sg != nil {
...
}
// 当前缓冲区已满
if c.qcount < c.dataqsiz {
...
}
...
// 获取当前goroutine对应的g结构体
gp := getg()
// 获取当前g的包装sudog
mysg := acquireSudog()
...
// 将发送的数据存入sudog的elem指针中,以便有接收协程可以直接取走数据
mysg.elem = ep
...
// 将当前sudog放入发送队列中
c.sendq.enqueue(mysg)
...
// 休眠当前goroutine
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
...
// 休眠结束后无需发送数据,因为数据已经被取走
// 释放sudog
releaseSudog(mysg)
...
return true
}
3.4 chan发送源码
整体代码如下:
// c <- x 是语法糖,在编译时会使用该函数替代
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
/* c: hchan结构体指针
* ep: 要发送的数据指针
*/
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 对一个未初始化的chan发送数据,如果是阻塞模式发送,将导致阻塞
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
... // 省略了调试相关代码
// 如果是非阻塞模式而且当前chan没有关闭而且缓冲区满了,直接返回
if !block && c.closed == 0 && full(c) {
return false
}
...
// 加锁,保护hchan中的其它字段
lock(&c.lock)
// 如果chan被关闭了,向关闭的chan写数据,直接panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 1.直接发送:如果接收者队列不为空,直接将要发送的数据交给接收者,而不存入缓冲区中
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 2.放入缓冲区:对于缓冲型的chan,没有接收者正在接收数据,而且缓冲区还有空间,将数据放入缓冲区
if c.qcount < c.dataqsiz {
// qp为指向缓冲区中下一个空闲位置的指针
qp := chanbuf(c, c.sendx)
...
// 将数据拷贝到缓冲区中
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
// 当前没有合适的缓冲区来存储发送的元素. 这个时候需要根据 "是否是阻塞发送" 来做出抉择
// 如果是 "阻塞发送", 那么就需要休眠当前的 goroutine
// 如果是 "非阻塞发送", 那么就快速失败返回(false)
if !block {
unlock(&c.lock)
return false
}
// 3.休眠等待: channel 满了,发送方会被放入发送队列,并休眠。
// 获取当前goroutine
gp := getg()
// 获取g的包装sudog
mysg := acquireSudog()
// 对sudog中的字段进行赋值
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 将要发送的数据直接存放再sudog的elem中,以便发送数据的协程可以直接获取数据
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 将当前goroutine加入等待发送队列
c.sendq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
// 休眠当前协程
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
KeepAlive(ep)
// 被唤醒后无需发送数据,因为数据已经被唤醒我们的协程取走了
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
// 释放sudog
releaseSudog(mysg)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}
// send处理向一个空的channel的发送操作
// 发送者要发送的ep被拷贝到接收者的goroutine
// 之后,接收者会被唤醒
// channel c肯定是空的(因为等待接收者的队列不为空)
// channel c肯定是被上了锁的,发送完成后使用lockf解锁
// sg为从接收者队列中取出的一个goroutine,它一定从c中取出来了,也就是c中没有它了
// ep为要发送的元素,ep一定不是空的,一定是指向堆或调用者的栈的
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
... // 调试相关代码
// 将数据发送给接收的协程
if sg.elem != nil {
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()
}
// 唤醒接收协程
goready(gp, skip+1)
}
// 向一个无缓冲的或有缓冲但是缓冲区为空的channel的发送和接收操作都会导致一个正在运行的goroutine
// 直接向另一个正在运行的goroutine的栈上写入数据。
// 由于 GC 假设对栈的写操作只能发生在 goroutine正在运行中并且由当前goroutine来写。
// 如果违反了该规则,使用写屏障将会是有效的,但是写凭证必须能起作用才行。
// 在typedmemmove中会调用typeBitsBulkBarrier,但是如果目标数据不在堆中,将会不起作用。
// 因此我们会使用memmove和typeBitsBulkBarrier来代替
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
// 直接进行内存"搬迁"
// 如果目标地址的栈发生了栈收缩,当我们读出了 sg.elem 后
// 就不能修改真正的 dst 位置的值了
// 因此需要在读和写之前加上一个屏障
dst := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
// 拷贝数据
memmove(dst, src, t.size)
}
chan的发送策略:
- 如果recvq有等待接收的goroutine,直接将数据拷贝给它。
- 如果队列中没有等待接收的goroutine且当chan是缓存型的而且缓冲区有空闲位置时,将数据拷贝到缓冲区中。
- 如果缓冲区中没有位置,就将当前goroutine加入等待发送队列中,挂起当前goroutine,直到当前goroutine被唤醒。
4、接收数据 <- c
接收操作有两种, 一种带 “ok”,表示 chan 是否关闭; 一种不带 “ok”, 这种写法, 当收到相应类型为零值时无法知道是真实发送者发送的值, 还是 chan 被关闭后, 返回接收者的默认类型的零值. 两种写法, 都有各种使用的场景.
- <- c 是一个语法糖
- 编译阶段,i <- c 被转化为runtime.chanrecv1()
- 编译阶段,i, ok <- c 被转化为runtime.chanrecv2()
- 两者最后都会调用chanrecv()函数
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
使用汇编来查看一下:
package main
import "fmt"
func main() {
channel := make(chan int, 5)
<- channel
ele1, _:= <- channel
_, ok1 := <- channel
ele2, ok2 := <- channel
fmt.Println(ele1, ele2, ok1, ok2)
}
上面两个函数最终都调用了chanrecv:
chan接收数据有四种情形:
有等待的G,从G中接收:
对于无缓冲的chan,发送者在发送数据时没有接收者,会被放入发送队列,此时接收者直接从发送者处接收数据。有等待的G,从缓存接收:
对于有缓冲的chan,缓冲已满,而且发送队列中有G正在等待,从缓存中接收数据。接收缓存:
缓冲中有数据,而且发送队列没有G,从缓存中接收阻塞接收:
:缓存中无数据,而且没有发送者,被放入接收队列并休眠。
4.1 有等待的G,从G中接收
这种情形如下,无缓冲的chan,有发送者在发送队列中休眠,接收者之间从发送队列中取出一个G,接收G中的数据并唤醒G。
代码如下:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 从发送队列中取一个G
if sg := c.sendq.dequeue(); sg != nil {
// 寻找一个发送等待者,如果buf空间为0,之间从发送者接收数据。
// 否则,从缓存中取出数据然后将发送者的数据放入环形缓冲区末尾
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
...
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// dataqsiz == 0, 也就是对于无缓冲的chan
if c.dataqsiz == 0 {
...
if ep != nil {
// 直接从发送者拷贝数据
recvDirect(c.elemtype, sg, ep)
}
} else {
...
}
// 唤醒发送者
goready(gp, skip+1)
}
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
...
// memmove 直接拷贝数据
memmove(dst, src, t.size)
}
4.2 有等待的G,从缓存中接收
这种情形如下:对于有缓冲的chan,此时缓冲已经满了,而且有发送者在发送队列中等待,接收者从队列中取出一个发送者G,然后从缓冲区中取走数据,并将发送者的数据写入环形缓冲区的尾部并唤醒发送者。之所以这样做,是因为chan是一个队列,缓冲区中的数据肯定是来的更早的,因此要优先从缓冲区中取数据。
代码如下:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 从发送队列中取出一个发送者,如果不为nil,如果有缓冲区,则要从缓冲区中取数据
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
...
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.dataqsiz == 0 {
...
} else { // 此时的情况是有缓冲
// 环形队列已满,从队列的头部取数据。将sender的数据放入队列的尾部
qp := chanbuf(c, c.recvx)
...
// 将数据从缓冲区拷贝到receiver中
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 将sender的数据拷贝到环形队列尾部
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()
}
// 唤醒sender
goready(gp, skip+1)
}
4.3 接收缓存
这种情形如下:对于有缓冲的chan,缓冲区中有数据,而且发送队列中没有发送者正在等待。因此,直接从缓冲区中将数据取走就可以了。
代码如下:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 发送队列中没有发送者,此时sg == nil
if sg := c.sendq.dequeue(); sg != nil {
...
}
// 缓存中有数据
if c.qcount > 0 {
// Receive directly from queue
// 直接从缓存中接收数据
qp := chanbuf(c, c.recvx)
...
// 将数据从缓存拷贝到接收者
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
}
...
}
4.3 阻塞接收
这种情形如下:缓冲区中没有数据或者没有缓冲区,而且发送队列中没有发送者,将接收者放入接收队列中休眠。当接收者被唤醒后,也无需再自己去接收数据,因为在被唤醒前,数据已经被唤醒我们的协程拷贝过去了。
代码如下:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 发送队列中没有发送者
if sg := c.sendq.dequeue(); sg != nil {
...
}
// 而且缓冲区中没有数据
if c.qcount > 0 {
...
}
...
// 没有发送者可用,阻塞在chan中
// 获取当前g
gp := getg()
// 获取g的包装sudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
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)
atomic.Store8(&gp.parkingOnChan, 1)
// 休眠当前goroutine
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 被唤醒后无需接收数据,因为数据已经被唤醒我们的协程给拷贝过去了
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
}
4.4 chan接收源码
整体代码如下:
// chanrecv从channel c中接收并将数据写入到ep
// ep可能为nil,对应的就是 <- c 或 _, ok := <- c 的场景,这种情况下接收的数据会被忽略
// 如果阻塞模式为非阻塞,那么如果没有数据可接收的情况下,返回 (true,false)
// 如果,channel c被关闭了,*ep被置0,并且返回 (ture,false)
// 否则,将ep指向的内存赋值为接收到的值,并返回(true,false)
// 一个非nil的ep必须指向堆内存或调用者的栈内存
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
... // 调试相关代码
// 如果c为ni且阻塞模式为非阻塞,直接返回(false,false)
if c == nil {
if !block {
return
}
// 否则,接收一个 nil 的 channel,goroutine 挂起
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回
// 当我们观察到 channel 没准备好接收:
// 1. 非缓冲型,等待发送列队 sendq 里没有 goroutine 在等待
// 2. 缓冲型,但 buf 里没有元素
// 之后,又观察到 closed == 0,即 channel 未关闭。
// 因为 channel 不可能被重复打开,所以前一个观测的时候 channel 也是未关闭的,
// 因此在这种情况下可以直接宣布接收失败,返回 (false, false)
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
return
}
if empty(c) {
// The channel is irreversibly closed and empty.
...
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
// channel 已关闭,并且数组 buf 里没有元素
// 这里可以处理非缓冲型关闭 和 缓冲型关闭但 buf 无元素的情况
// 也就是说即使是关闭状态,但在缓冲型的 channel,
// buf 里有元素的情况下还能接收到元素
if c.closed != 0 && c.qcount == 0 {
...
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// 等待发送队列里有 goroutine 存在,说明 buf 是满的
// 这有可能是:
// 1. 非缓冲型的 channel
// 2. 缓冲型的 channel,但 buf 满了
// 针对 1,直接进行内存拷贝(从 sender goroutine -> receiver goroutine)
// 针对 2,接收到循环数组头部的元素,并将发送者的元素放到循环数组尾部
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// 缓冲型,buf 里有元素,可以正常接收
if c.qcount > 0 {
// 直接从buf中获取数据
qp := chanbuf(c, c.recvx)
...
// 将数据从buf拷贝到receiver中
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
}
// buf中无数据,而且没有发送者在等待,接收者阻塞在当前chan中
gp := getg()
// 获取g的包装sudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 对sudog的字段赋值
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)
atomic.Store8(&gp.parkingOnChan, 1)
// 休眠当前协程
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 下面代码为接收者被唤醒后的逻辑,此时唤醒我们的协程已经将数据拷贝给我们了,因此无需自己去接收
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处理两种接收操作从一个满的chan中
// 两种操作如下:
// 1) 接收者从buf中接收数据,发送者将数据写入buf尾部,然后发送者被唤醒继续自己的业务
// 2) 接收者直接从发送者处接收数据。
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.dataqsiz == 0 {
...
if ep != nil {
// 直接从sender中拷贝数据
recvDirect(c.elemtype, sg, ep)
}
} else {
// buf已满,从中取出一个数据,然后将sender的数据存入,唤醒sender
qp := chanbuf(c, c.recvx)
...
// 接收者从buf中拷贝数据
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 从发送者拷贝数据到buf中
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()
}
// 唤醒sender
goready(gp, skip+1)
}
5、关闭chan
func closechan(c *hchan) {
// 关闭一个为nil的chan引发panic
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
// 关闭一个已经关闭的chan也会引发panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
...
c.closed = 1
var glist gList
// 释放所有的接收者
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)
}
// 释放所有的发送者,它们会引发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)
// 唤醒glist中的g
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
在关闭chan的源码中,如果向一个nil或已经关闭的chan再次调用close都会引发panic