Channel 是支撑 Go 语言高性能并发编程模型的重要结构,Channel 是一个用于同步和通信的有锁队列,使用互斥锁解决程序中可能存在的线程竞争问题。源码路径runtime/chan.go
CSP 模型
CSP 模型全称为 communicating sequential processes,CSP 模型由并发执行实体(进程,线程或协程),和消息通道组成,实体之间通过消息通道发送消息
channel读写
ch := make(chan int)
// write
ch <- x
// read
x <- ch
// another read
x = <- ch
注意: channel 初始化后才能进行读写操作,否则阻塞
关闭 channel
ch := make(chan int)
close(ch)
- 关闭未初始化 channel 会产生 panic
- 重复关闭 channel 产生 panic
- 向已关闭 channel 中发送会产生 panic
- 从已关闭的 channel 读取不会 panic,能读出 channel 未被读取的,若消息均已读出则会读到类型的零值。从已关闭的 channel 中读取不会阻塞,会返回一个为 false 的 ok-idiom(判断 channel 是否关闭)
- 关闭 channel 产生广播机制,所有读取channel的 goroutine 都会收到消息
0. 结构体
hchan结构体,Channel 实际上是个环形队列。
- qcount — Channel 中的元素个数
- dataqsiz — Channel 中的循环队列的大小
- buf — Channel 的缓冲区数据指针
- sendx — Channel 的发送操作的位置
- recvx — Channel 的接收操作的位置
- elemsize — 当前 Channel 能够收发的元素类型
- elemtype — Channel 能够收发的元素大小
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 // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// 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
}
sendq 和 recvq 表示 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,使用双向链表 runtime.waitq 表示,元素都是 runtime.sudog 结构
type waitq struct {
first *sudog
last *sudog
}
1. 创建管道 — makechan函数
Channel 的创建都会使用 make 关键字,如果不向 make 传递缓冲区大小的参数,那么就会设置一个默认值 0,也就是当前的 Channel 不存在缓冲区。
case TCHAN:
l = nil
if i < len(args) {
l = args[i]
i++
l = typecheck(l, ctxExpr)
l = defaultlit(l, types.Types[TINT])
if l.Type == nil {
n.Type = nil
return n
}
if !checkmake(t, "buffer", &l) {
n.Type = nil
return n
}
n.Left = l
} else {
n.Left = nodintconst(0)
}
n.Op = OMAKECHAN
}
调用 runtime.makechan 或者 runtime.makechan64 的函数,makechan64 处理缓冲区大小大于 2 的 32 次方的情况,重点关注 runtime.makechan 函数
case OMAKECHAN:
// When size fits into int, use makechan instead of
// makechan64, which is faster and shorter on 32 bit platforms.
size := n.Left
fnname := "makechan64"
argtype := types.Types[TINT64]
// Type checking guarantees that TIDEAL size is positive and fits in an int.
// The case of size overflow when converting TUINT or TUINTPTR to TINT
// will be handled by the negative range checks in makechan during runtime.
if size.Type.IsKind(TIDEAL) || maxintval[size.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
fnname = "makechan"
argtype = types.Types[TINT]
}
n = mkcall1(chanfn(fnname, 1, n.Type), n.Type, init, typename(n.Type), conv(size, argtype))
1.1 合法性验证
- 数据类型大小,大于1<<16时异常
- 内存对齐(降低寻址次数),大于最大的内存8字节数时异常
- 传入的size大小,大于堆可分配的最大内存时异常
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 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")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
1.2 分配地址空间
根据 channel 中收发元素的类型和缓冲区的大小初始化 runtime.hchan 和缓冲区
- 如果 channel 不存在缓冲区,分配 hchan 结构体空间,即无缓存 channel
- 如果 channel 存储的类型不是指针类型,分配连续地址空间,包括 hchan 结构体 + 数据
- 默认情况包括指针,为 hchan 和 buf 单独分配数据地址空间
// 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)
}
更新 hchan 结构体的数据,包括 elemsize elemtype 和 dataqsiz
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
2. chansend函数
操作如: ch <- x。 编译器解析成 OSEND 并在 cmd/compile/internal/gc/walk.go 转换成 chansend1 函数。
case OSEND:
n1 := n.Right
n1 = assignconv(n1, n.Left.Type.Elem(), "chan send")
n1 = walkexpr(n1, init)
n1 = nod(OADDR, n1, nil)
n = mkcall1(chanfn("chansend1", 2, n.Left.Type), nil, init, n.Left, n1)
// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
chansend 函数内容相当多,可以归纳为三部分:
- 当存在等待的接收者时,也就是在 recvq 可以获得 waitq,通过 send 方法直接将数据发送给等待的接收者
- 当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区
- 当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据
/*
* 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 {
未创始化为nil,向其中发送数据将会阻塞
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
这是队列满的情况
// 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
}
已经关闭channel,向其中发送将panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
2.1 chansend 主要内容一
当存在等待的接收者时,也就是在 recvq 可以获得 waitq,通过 send 方法直接将数据发送给等待的接收者(第3节讲解)方法
goroutine 阻塞在 channel 上,直接将数据发送给该 goroutine,从当前 channel 的等待队列中取出等待的 goroutine,然后调用 send。
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
}
2.2 chansend 主要内容二
当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区
如果创建的 Channel 包含缓冲区并且 Channel 中的数据没有装满,typedmemmove 将发送的数据拷贝到缓冲区中并增加 sendx 索引和 qcount 计数器
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
}
2.3 chansend 主要内容三 阻塞发送
- getg 获取发送数据的 Goroutine
- acquireSudog 获取 sudog 结构
- 将创建并初始化的 sudog 加入发送等待队列,并设置到当前 Goroutine 的 waiting上,表示 Goroutine 正在等待该 sudog 准备就绪
- gopark 将当前的 Goroutine 陷入沉睡等待唤醒
- 被调度器唤醒后会将一些属性置零并且释放 runtime.sudog 结构体
// 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)
// 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
3. send函数
- 调用 sendDirect 将发送的数据直接拷贝到接收方 sg 中
- 调用 goready 将等待接收数据的 Goroutine 标记成可运行状态 Grunnable, 并把该 Goroutine 放到发送方所在的处理器的 runnext 上,该处理器调度时会唤醒接收方
// send processes a send operation on an empty channel c.
// The value ep sent by the sender is copied to the receiver sg.
// The receiver is then woken up to go on its merry way.
// Channel c must be empty and locked. send unlocks c with unlockf.
// sg must already be dequeued from c.
// ep must be non-nil and point to the heap or the caller's stack.
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)
}
4. closechan函数
4.1 close未初始化channel产生panic,关闭已经closed的channel将产生panic
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"))
}
4.2 释放所有读取
// 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())
}
gp.schedlink.set(glist)
glist = gp
}
4.3 释放所有写
// 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())
}
gp.schedlink.set(glist)
glist = gp
}
4.4
// Ready all Gs now that we've dropped the channel lock.
for glist != nil {
gp := glist
glist = glist.schedlink.ptr()
gp.schedlink = 0
goready(gp, 3)
}
5. chanrecv函数
x := <-ch
5.1 未初始化channel,阻塞
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
5.2 从已经关闭的且书记已经都读出,则返回数据类型的0值
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
}
5.3 当前有发送 goroutine 阻塞在 channel 上,说明buf 已经满员
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
}
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
}