golang channel底层剖析
内存模型
type hchan struct {
qcount uint // queue 里面有效用户元素,这个字段是在元素出对,入队改变的;
dataqsiz uint // 初始化的时候赋值,之后不再改变,指明数组 buffer 的大小;
buf unsafe.Pointer // 指明 buffer 数组的地址,初始化赋值,之后不会再改变;
elemsize uint16 // 指明元素的大小,和 dataqsiz 配合使用就能知道 buffer 内存块的大小了;
closed uint32
elemtype *_type // 元素类型,初始化赋值;
sendx uint // send index
recvx uint // receive index
recvq waitq // 等待 recv 响应的对象列表,抽象成 waiters
sendq waitq // 等待 sedn 响应的对象列表,抽象成 waiters
// 互斥资源的保护锁,官方特意说明,在持有本互斥锁的时候,绝对不要修改 Goroutine 的状态,不能很有可能在栈扩缩容的时候,出现死锁
lock mutex
}
send和recv的流程
初始状态下,ch的缓冲区为空,读和写的下标都指向下标0的位置,等待队列也都为空。
然后一个协程g1向ch中发送数据,因为没有协程在等待接收数据,所有元素会被存入缓冲区中,sendx
会从0开始向后挪,第五个元素会放到下标为的4的位置,然后sendx会重新回到0。
此时缓冲区中已经没有空闲的位置了。所以接下来发送的第6个元素无处可放,g1会进到ch的发送等待队列中。
发送等待队列是一个sudog类型的链表。
type sudog struct {
g *g // 记录哪个协程在等待
isSelect bool
next *sudog
prev *sudog
elem unsafe.Pointer // 等待发送的信息在哪
acquiretime int64
ticket uint64
parent *sudog
waitlink *sudog
waittail *sudog
c *hchan // 等待哪一个channel
}
里面会记录哪个协程在等待,等待哪一个channel,等待发送的信息在哪等信息。
接下来协程g2从ch接收一个元素,recvx指向下一个位置,第0个位置就空出来了。所以会唤醒sendq中的g1,将这里的数据发送给ch。然后缓冲区再次满了,sendq队列为空。
在这一过程中,可以看到sendx
和recvx
,都会从0到4再到0这样循环变化,所以channel的缓冲区被称为环形缓冲区
。
发送数据
所以像诸如 ch <- 10
这样给channel发送数据时,只有在缓冲区还有空闲位置,或者有协程在等着接收数据的时候才不会发生阻塞
。
碰到channel为nil,或者ch没有缓冲区,而且也没有协程等着接收数据,又或者ch有缓冲区但缓冲区已用尽的情况,都会发生阻塞
。
那如果不想阻塞的话,就可以使用select关键字。
select {
case ch <- 10;
...
default:
...
}
使用这种写法时,如果检测到ch可以发送数据,就会执行case分支;如果会阻塞,就会执行default分支了。
接收数据
接收数据的写法要更多一些。
// 将结果直接丢弃
<-ch
// 将结果赋值给变量v
v := ch
// common ok风格写法
// ok为false时表示ch已经关闭,此时v是channe元素类型的零值
v, ok := <-ch
以上几种写法都允许发生阻塞,只有在缓冲区中有数据,或者有协程等着发送数据时,才不会发生阻塞。如果channel为nil,或者channel无缓冲而且没有协程等着发送数据,又或者channel有缓冲但是缓冲区无数据时,都会发生阻塞。
如果无论如何都不想阻塞,t=同样可以采用select关键字。这样在检测到channel的recv操作不会阻塞时,就会执行case分支;如果会阻塞,就会执行default分支。
多路select
多路select指的是存在两个或更多的case分支,每个分支可以是一个channel的send或者recv操作。
例如一个协程通过多路select,等待ch1和ch2。这里default分支是可选的,我们暂且把这个协程记为g1。
var a, b int
b = 10
select {
case a = <-ch1:
println(v)
case ch2 <- b:
default:
}
多路select会被编译器转换为对runtime.selectgo
函数调用。
func selectgo(cas0 *scase, order0 *unint16, pc0 *unintptr, nsends, nrecvs int, block bool) (int, bool)
第一个参数cas0指向一个数组,数组里面装的是select中所有的case分支。顺序是send在前recv在后。
第二个参数order0指向一个uint16类型的数组,数组大小等于case分支的两倍,实际上被用作两个数组。第一个数组用来对所有channel的轮询进行乱序。第二个数组用来对所有channel的加锁操作进行排序。因为轮询需要乱序才能保障公正性。而按照固定算法确定加锁顺序才能避免死锁。
第三个参数和race检测有关,我们暂且不关心。
剩下的nsends和nrecvs分别表示所有case中,执行send和recv操作的分支分别有多少个。
block表示多路select是否需要阻塞等待,对应到代码只能就是有default分支的不会阻塞,没有的会阻塞。
再来看第一个返回值,它代表最终哪个case分支被执行了。
第二个返回值用于在执行recv操作的case分支时,表明是实际接收到了一个值,还是因channel关闭而得到了零值。
多路select需要进行轮询来确定哪个case分支可操作了。但是轮询前要先加锁,所以selectgo函数在执行时,会先按照有序的加锁顺序,对所有的channel加锁,然后按照乱序的轮询顺序,检查所有channel的等待队列和缓冲区。
假如检查到ch1时,发现有数据可读,那就直接拷贝数据,进入对应分支;假如所有channel都不可操作,就把当前协程添加到所有channel的sendq或recvq中。
对应到这个例子,g1会被添加到ch1的recq,以及ch2的sendq中。之后g1会挂起,并解锁所有的channel。
假如接下来的ch1有数据可读了,g1就会被唤醒,完成对应的分支操作后,会再次按照加锁顺序对所有channel加锁,然后从所有sendq或recvq中将自己移除。最后全部解锁后返回。
channel源码阅读
创建channel
创建channel的源码在runtime/chan.go
文件中的makechan
函数,该函数在分配好内存之后,直接返回一个runtime.hchan
类型的指针:
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 最多存65535个元素
// 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")
}
// 查看需要的缓冲区是否越界
// math.MulUintptr会将两数相乘,越界的话overflow是负数
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的整个发送过程和接收过程都使用了runtime.mutex进行加锁。runtime.mutex是runtime相关源码中常用的一个轻量级锁。
不管是有缓冲的channel还是无缓冲的channel,他们发送元素的源码都是一样的,都会调用chansend
函数:
// ep是待发送数据的首地址
// block代表写入操作是否阻塞
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 如果channel为nil
if c == nil {
// 如果写入操作不阻塞,就返回false
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, funcPC(chansend))
}
// 如果block非阻塞
// 且channel没有关闭
// 且(channel非缓冲队列且接收队列receiver为空)或者(channel为有缓冲队列且buf已满 )
// 直接返回false
if !block && c.closed == 0 && full(c) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
// 获取同步锁,保证协程安全
lock(&c.lock)
// 如果通道已经关闭,写入数据产生panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 核心部分
// 如果接收队列recvq不为空,即有goroutine在接收队列中等待时
// 这里不用区分有缓冲和无缓冲channel
// 跳过缓冲区,直接将数据发送给等待的接收者goroutine
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 如果接收队列recv为空
// 且缓冲区数据大小 < 通道大小,说明此时发送队列为空
if c.qcount < c.dataqsiz {
// 直接将数据放入缓冲区
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 数据转移:本质上是内存拷贝,ep处拷贝到qp处
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
// 如果以上条件都不满足
// 即接收队列为空,且缓冲区buf数据量大小 == 通道大小
// 如果block为非阻塞,解锁并返回false
if !block {
unlock(&c.lock)
return false
}
// 如果接收队列recv不为空
// 且缓冲队列已满
// 则将当前的goroutine加入到sendq队列
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 将当前goroutine的sudog加入到sendq
c.sendq.enqueue(mysg)
// 将当前goroutine的sudog加入到sendq
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
releaseSudog(mysg)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}
总结:
- 判断 channel 是否是nil,如果是,则会永久阻塞导致死锁报错。
- 如果 channel 中 recvq 存在接收者 goroutine,则直接把需要发送的数据拷贝到接收者 goroutine,recvq 储存的其实是一个以 sudog 结构为节点的链表,sudog结构中保存了接收者 goroutine 的指针。
- 如果 recvq 中不存在接收者:
- 如果buf没有满,就直接把数据拷贝到 buf 的 sendx 位置。
- 如果 channel 为无缓冲 channel 或 buf 已满,则把当前 goroutine 保存到 sendq 等待队列中,阻塞当前 goroutine。
接收元素
接收元素会调用chanrecv
函数:
// block表示当channel无法返回数据的时候是否阻塞等待
// 比如当block为false并且channel中没有数据时,直接返回
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if debugChan {
print("chanrecv: chan=", c, "\n")
}
// 如果通道为空或未初始化
if c == nil {
// 如果block为非阻塞
// 直接返回
if !block {
return
}
// 如果block为阻塞,调用gopark()阻塞当前goroutine
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 如果通道不为空或者已经初始化
// 如果block为非阻塞接收
// 且((通道为无缓冲通道且发送对列为空) 或者 (通道为有缓冲通道且缓冲区元素为0且通道未关闭))
// 直接return false false
if !block && empty(c) {
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)
// 核心原理
// 如果通道已经关闭
// 且缓冲区中无元素
// 直接返回true和false(非正常返回)
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
// 返回空值
// typedmemclr根据类型清理相应的地址的内存
typedmemclr(c.elemtype, ep)
}
return true, false
}
// 如果等待发送队列sendq不为空(此时缓冲区一定是满的)
// 两种情况:
// 1.非缓冲型channel,直接进行内存拷贝
// 2.缓冲型channel,但buf满了,则接收buf头部的元素,并将发送队列头goroutine的元素放到循环数组的尾部
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// 如果等待发送队列sendq为空
// 且通道非空 (注意: 一定是缓冲型channel)
// 则从通道中获取数据
if c.qcount > 0 {
// 从循环数组里找到要接收的元素
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
// ep不为nil,说明未忽略要接收的值,即val<-ch,非<-ch
if ep != nil {
// 内存拷贝
typedmemmove(c.elemtype, ep, qp)
}
// 清理循环数组recvx处的值
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
// 如果等待发送队列sendq为空
// 且通道中元素个数为0,通道为空
// 且block为非阻塞
// 则直接返回false,false
if !block {
unlock(&c.lock)
return false, false
}
// 如果等待发送队列sendq为空
// 且通道中元素个数为0,通道为空
// 且block为非阻塞
// 则直接返回false,false
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 保存待接收变量的地址ep
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// goroutine加入等待接收队列
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
}
总结:
- 判断 channel 是否是nil,如果是,则会永久阻塞导致死锁报错。
- 如果 channel 中 sendq 有等待发送数据的 goroutine:
- 如果是无缓冲 channel,则直接把要发送的数据拷贝到接受者的 goroutine 中,并唤醒发送方 goroutine。
- 如果是有缓冲的 channel(说明此时缓冲区满了),则把 buf 中 recvx 位置的数据拷贝到 buf 中的 sendx 位置,并唤醒发送的 goroutine。
- 如果 channel 中 sendq 没有等待发送数据的 goroutine:
- 如果 buf 有数据,则把 buf 中的 recvx 位置的数据拷贝到当前的接收goroutine。
- 如果 buf 没有数据,则把当前 goroutine 加入 recvq 等待队列中,并挂起。