写在前头:
- 代码解析写在代码中:用【】的形式,【xxxxxxxx】;
- 需要拓展的解析:用【1】序号的形式,【1】、【2】;然后在源码之后给出解释;
- 每个【】中解释的是:当前【】之下,直到下一个【】之间的代码段;
上图同样来自bing每日壁纸~
最近的文章有点偏题了。我们回到并发模型,上次发了两篇文章介绍并发模型——CSP
甘蔗:go语言并发原理和机制【一】zhuanlan.zhihu.com我们知道CSP模型中的精髓就是:
Don't communicate by sharing memory, share memory by communicating.
那么go语言是如何实现上述“利用通信进行内存共享”的呢?答案是利用通道。
通道(channel)是 Golang 实现 CSP 并发模型的关键,⿎励⽤通讯来实现数据共享。可以说,缺了 channel,goroutine 会黯然失⾊。
下面我将从源码角度解释Go语言对“通道”的抽象。
1. 通道的创建
Go代码中,创建通道语句十分简单:
ch := make(chan type, int)
type是通道传输数据类型,int是通道容量。详情看我之前的文章吧。
如果我们在代码中调用了make就会生成一个通道。
源码1-chan的定义
type hchan struct {
qcount uint 【总计数】// total data in the queue
dataqsiz uint 【缓存槽大小】// size of the circular queue
buf unsafe.Pointer 【缓存槽指针】// points to an array of dataqsiz elements
elemsize uint16【数据项大小】
closed uint32【通道关闭信息】
elemtype *_type【数据项类型】
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
【1】
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
【1】lock mutex
很明显这是一个锁。并且类型mutex还是一个互斥锁。它保护hchan中的所有字段和处于sudog状态的、用到了此通道的goroutine中的一些字段。
ps:sudog是一个goroutine状态:表示等待列表中的g,例如在一个通道中用于发送/接收。
源码2-chan的创建
func makechan(t *chantype, size int) *hchan {
【获得通道类型】
elem := t.elem
【检查数据项大小,不能超过64kb】
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
【检查对齐】
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
【1】
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
【2】
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)
【如果设置了debug就输出chan信息】
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "n")
}
return c
}
【1】
对于【1】,是在检查内存是否溢出。其中函数MulUintptr表示两个参数的乘积,并判断是否溢出。其定义如下:
func MulUintptr(a, b uintptr) (uintptr, bool) {
if a|b < 1<<(4*sys.PtrSize) || a == 0 {
return a * b, false
}
overflow := b > MaxUintptr/a
return a * b, overflow
}
a和b中其中一个不能大于65536,或者a(就是数据项目数量)等于0,那么返回 a*b和false。mem表示数据项*数据项大小,就是数据内存大小。
【2】
受到垃圾回收机制的限制,缓冲槽必须单独分配内存。
- mem为0就申请大小为 hchanSize 的空间,缓存指针即为chan;
- 如果元素不包含指针,就调整 buf 指针,chan + 数据内存占用;
- 其他就是正常的;
2. 数据的收发
2.1 涉及结构体
通道收发数据必须对 channel 双方进行封装,这涉及到goroutine的 g 结构体和 sudog 结构体中一些参数:
type g struct {
param unsafe.Pointer // passed parameter on wakeup
}
sudo中的注释也说了,以下某些字段受 hchan.lock 保护,他阻塞在channel中;并且堆栈的收缩也是取决于 channel 状态。
type sudog struct {
// The following fields are protected by the hchan.lock of the
// channel this sudog is blocking on. shrinkstack depends on
// this for sudogs involved in channel ops.
g *g
elem unsafe.Pointer // data element (may point to stack)
}
另外 channel 还的维护发送和接受者等待队列,以及异步缓冲槽环状队列索引。
type hchan struct {
sendx uint // send index【发送槽位置索引】
recvx uint // receive index【接收槽位置索引】
recvq waitq // list of recv waiters【接收者等待队列】
sendq waitq // list of send waiters【发送者等待队列】
}
type waitq struct {
first *sudog
last *sudog
}
2.2 发
【发数据给chan】
// 调用下面语句,编译器将调用【chansend()】
chan <- x
// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
收发其实很easy啦。匹配到收发双方就行。然后就是熟练的运用阻塞挂起技能就行。
下面这个函数会由 chansend1 调用。
一句话总结:发送数据时候,会先在通道的接收队列中寻找接收者;没有就打包成为sudog存储在发送队列之中。
有人说相当于一个两级管理模式?你品,你细品。
c:通道指针,ep:参数指针,block:是否阻塞,callerpc:函数指针;返回 bool 类型。
/*
* generic single channel send/recv
* If block is not nil,
* then the protocol will not
* sleep but return if it could
* not complete.
*
* sleep can wake up with g.param == nil
* when a channel involved in the sleep has
* been closed. it is easiest to loop and re-run
* the operation; we'll see that it's now closed.
*/
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
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))
}
【1】
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not closed, we observe that the channel is
// not ready for sending. Each of these observations is a single word-sized read
// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
// Because a closed channel cannot transition from 'ready for sending' to
// 'not ready for sending', even if the channel is closed between the two observations,
// they imply a moment between the two when the channel was both not yet closed
// and not ready for sending. We behave as if we observed the channel at that moment,
// and report that the send cannot proceed.
//
// It is okay if the reads are reordered here: if we observe that the channel is not
// ready for sending and then observe that it is not closed, that implies that the
// channel wasn't closed during the first observation.
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false
}
// 【这个 t0 不知道,好像是cpu打点计时器】
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
// 【通道上锁】
lock(&c.lock)
// 【通道关闭,抛错】
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
【2】
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
【3】
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
// 【如果上述都没有发生,就是没有关闭,没有成功发送(找不到接收者),又没有存入缓存;
// 那只能说明,代码调用的是没有缓存的通道,这个时候应该阻塞才对;下面判断就是是否阻塞。】
if !block {
unlock(&c.lock)
return false
}
【4】
// Block on the channel. Some receiver will complete our operation for us.
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
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
KeepAlive(ep)
【5】
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true
}
【1】
对于这个判断的意思首先要明白,我们创建一个通道 c chan,可以定义它的容量,也就是 dataqsiz;默认是没有容量的,这个时候就会堵塞在这里,也就是 block 的值应该为 true;如果有容量,传输数据量也不应该大于它;他如果满了,也应该阻塞。所以这个判断的意思就是:如果:
- !block :通道不阻塞模式;
- c.closed ==0 :通道没关闭;
- (c.dataqsiz ==0&& c.recvq.first ==nil) 或者 (c.dataqsiz >0&& c.qcount == c.dataqsiz):容量为0并且没有接收其他数据,或者,容量有但是容量已满。
上述中如果全部出现,那么就错了,直接返回 False。
【2】
获得通道 c 的接受队列,然后调用 send函数;send函数基本上和本函数后面一样,就不多说了。
【3】判断(存量<容量)
数据送入缓存操作。
流程依此过一遍吧。
- chanbuf (),返回位于 c.sendx 位置的指针;
- raceenabled 用于运行go语言时,加 -race 命令跟踪程序数据;
- typedmemmove(c.elemtype, qp, ep),将 ep 数据拷贝到 qp;
- 后面就是存量自加,sendx 自加,解锁等一系列基本操作;
【4】
基于程序运行到这里,还没有返回,说明什么?
说明这个通道没有缓存,没有被接收,它在等待接收。
那么此时go语言是怎么做呢?
Go语言创建了一个 sudog,来替代原来的goroutine!这样一来,原来的goroutine便可以继续执行其他代码,而不用阻塞(如果你是这样设计代码的话),从而达到异步的效果。
流程依次过一遍吧。
- 获得当前goroutine,gp;获得sudog(没有现存的就新建一个)mysg;
- releasetime 不知道啥东西;
- 依此拷贝 ep,gp,c;
- 将 mysg 加入 gp 的等待队列;
- 将 mysg 加入 c 的发送等待队列;
- goparkunlock () 阻塞等待被唤醒;
- KeepAlive (ep) 用来给 ep 保活;
- 等待唤醒;
【5】被唤醒
因为期间 sudog 可能被人用过,所以判断一下mysg 是否等于 gp.waiting;
进一步再判断一下是否是被 closechan () 唤醒;
最后,确认数据已经传输后,releaseSudog(mysg) 将sudog放回复用。
2.3 收
【从chan收数据】
// 调用下面语句,编译器将调用函数【chanrecv1()】
x <- chan
// 调用下面语句,编译器将调用函数【chanrecv2()】
x, bool <- chan
相比 chanrecv1() ,chanrecv2()比它多返回了一个 Bool 类型的 received,以判断是否正常接收数据。
// entry points for <- c from compiled code
//go:nosplit
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
}
来看看 chanrecv 源码哦。
// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
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.
【1】
if debugChan {
print("chanrecv: chan=", c, "n")
}
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
【2】
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not ready for receiving, we observe that the
// channel is not closed. Each of these observations is a single word-sized read
// (first c.sendq.first or c.qcount, and second c.closed).
// 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.
//
// The order of operations is important here: reversing the operations can lead to
// incorrect behavior when racing with a close.
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
【3】
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
}
【4】
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
}
【5】
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
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
}
【6】
// no sender available: block on this 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)
goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)
【7】
// someone woke us up
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
closed := gp.param == nil
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, !closed
}
【1、2、4、5、6】
对于这些代码部分,其实可以完全参照“发送”部分代码的解释;
【3】
对于【3】我想解释的是,c.qcount 是代表此时通道 c 中有多少个数据;发送和接收都用到了它,仔细看。如果 qcount 等于0,说明没有数据;那还接收个毛线~~~
【7】
对于【7】,我想解释一下 gp.param:这个参数负责唤醒,接收完成,他应该等于 nil;
3. 关闭
func closechan(c *hchan) {
// 【不能关闭 空 通道】
if c == nil {
panic(plainError("close of nil channel"))
}
// 【上锁】
lock(&c.lock)
// 【不能多次关闭】
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
// 【加入 race 路径】
if raceenabled {
callerpc := getcallerpc()
racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
racerelease(c.raceaddr())
}
// 【设置 closed 为 True】
c.closed = 1
var glist gList
// 【释放所有 接收者】
// release all readers
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 = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// 【释放所有 发送者】
// release all writers (they will 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 = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
关闭通道的代码解释写在上面~~还挺简单的,不多说了。