基本概念
Don’t communicate by sharing memory, share memory by communicating
基本架构
channel
本质上的是一个环形队列,参考下面的代码:
type hchan struct {
qcount uint // 队列中持有的数据数量
dataqsiz uint // 环形队列的大小,创建带buffer的channel,值为第2个参数
buf unsafe.Pointer // 环形队列指针,使用带buffer的channel时,用于缓存数据
elemsize uint16 // channel类型大小
closed uint32 // channel是否已经关闭:0=未关闭,1=关闭
elemtype *_type // 元素类型
sendx uint // 环形队列发送端的元素下标
recvx uint // 环形队列接收端的元素下标
recvq waitq // 阻塞的goroutine的等待队列,尝试从channel读取数据
sendq waitq // 阻塞的goroutine的等待队列,尝试向channel写入数据
lock mutex // 锁,保证线程安全
}
// 表示一个在等待链表中的goroutine,该结构中存储了两个分别指向前后 runtime.sudog 的指针以构成链表。
type waitq struct {
first *sudog // g链表头部
last *sudog // g链表尾部
}
// sudog是对goroutine上下文的封装,表示被挂起的goroutine对象
// 一个goroutine可能在多个等待列表中,所以多个sudog可能等待一个goroutine
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
...
}
sendx
和recvx
会用来计算缓冲区下一个元素写入或读取的位置,源码如下:
// ...
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)
// ...
func chanbuf(c *hchan, i uint) unsafe.Pointer {
return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
return unsafe.Pointer(uintptr(p) + x)
}
计算方法的原理是缓冲区buf
的指针位置加上索引下标i(sendx/recvx)乘以chan
元素大小的值:
nextElemPtr = bufPtr + i * elemsize
相当于
c.buf[c.sendx]
c.buf[c.recex]
随后调用typedmemmove
方法操作这个地址对应的元素:
typedmemmove(c.elemtype, ep, qp)
表示buf
中的当前可读元素拷贝到接收变量的地址处。typedmemmove(c.elemtype, qp, sg.elem)
表示将sendq
中goroutine
等待发送的数据拷贝到buf
中。
创建channel
安全检查
创建channel
分为2个方法:runtime.makechan
和runtime.makechan64
,后者是在前者的基础上添加了d检查chan size范围的逻辑:
func makechan64(t *chantype, size int64) *hchan {
// int范围等于操作系统位置(64位系统为8字节,32位系统位4字节)
if int64(int(size)) != size {
panic(plainError("makechan: size out of range"))
}
return makechan(t, int(size))
}
创建流程
- 安全检查: channel能存的元素类型大小是否超过2^16
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
- 判断是否内存对齐(即hchan实际大小是否为maxAlgign=8的整数倍)
hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
- 上面的代码可以看成
hchanSize = chansize + maxAlign - (size % maxAlgin)
。 - 关于
uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
的含义,实际是这是位运算中,2的n次幂减去hchan size
对2的n次幂取模的值:- 这里2^n = 8
- x为hchan实际的size大小
- 对2的n次幂取模,公式为
x & (2^n - 1)
所以可以抽象为x mod=2^n + (-(x & (2^n - 1)))
随后是对chan size是否对齐的校验
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
- 计算和判断所需空间
mem
是否超过最大可申请空间,判断size是否小于0(非法)
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
- 申请hchan所需内存空间
- 无缓冲channel:给hchan申请大小为
hchansize
的内存空间 - 有缓冲channel:
- 元素为非指针类型
(elem.ptrdata == 0)
,申请hchansize+mem
大小的连续内存空间, 并将hchanSize之后的首地址赋值给buf
- 元素类型是指针,为
hchan
和底层buf
申请连续的内存空间(默认分配策略)
- 元素为非指针类型
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)
}
- 更新
chan
的elemsize
、elemtype
和dataqsiz
字段以及初始化lock
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
向channel发送消息
向channel发送消息时,处理逻辑的主体方法为chansend
:
c *hchan
发送消息的目标channel
ep unsafe.Pointer
发送元素的指针对象block
是否为阻塞发送callerpc
是创建channel
对应go
语句的地址
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ...
}
- 初始化判断
// channel是否已经关闭
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// channel是否已经准备好发送消息
if raceenabled {
racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
}
// 如果channel没有准备好发送且已经关闭,同时channel被阻塞
if !block && c.closed == 0 && full(c) {
return false
}
//加上锁
lock(&c.lock)
// channel已经关闭,panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
- 消息发送
向
channel
消息发送时,存在3种情况:- 同步发送:当前
recvq
刚好有等待的goroutine
,直接调用send
发送消息 - 异步发送:当前
channel
中的元素个数小于其循环队列的长度,则将数据放入队列中 - 阻塞发送:如果不满足上面的2种情况,会创建
sudog
并将其加入channel
的sendq
队列中,阻塞当前goroutine
等待其他goroutine
从channel
接收数据
- 同步发送:当前
同步发送
- 加锁保证线程安全
- 检查 channel 是否关闭。如果关闭则抛出 panic。
- 尝试从
recvq
队列获取等候groutine
, 直接唤醒goroutine
并发送数据
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
if sg := c.recvq.dequeue(); sg != nil {
// 直接把当前消息发给的goroutine
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
sendDirect
执行将将发送的数据(eq)
拷贝到接收变量的内存地址上goready
执行将等待接收的阻塞goroutine
的状态从Gwaiting
或者Gscanwaiting
设置为Grunnable
。下一轮调度时会唤醒这个接收的goroutine
。
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)
}
关于goready
的实现,会把当前goroutine
绑定到本地可运行的队列中,等待下次调度便立即运行。这样做可以保证goroutine
的线程安全,但是在读取数据方面存在延迟。
异步发送
如果channel
中的数据没有达到上限,则根据sendx
对应的元素指针将数据拷贝到缓冲区中。同时更新sendx
和qcount
,将缓冲区形成环,这样索引值到了队尾,下标则重置为队头。
如果 qcount还没有满,则调用 chanbuf() 获取 sendx 索引的元素指针值。调用 typedmemmove() 方法将发送的值拷贝到缓冲区 buf 中。拷贝完成,需要维护 sendx 索引下标值和 qcount 个数。这里将 buf 缓冲区设计成环形的,索引值如果到了队尾,下一个位置重新回到队头。
// 如果队列还有空间, 就把数据放入到队列中
if c.qcount < c.dataqsiz {
// 计算当前sender队列的指针位置
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 根据当前sender队列的指针位置,将发送的数据拷贝到缓冲区中
typedmemmove(c.elemtype, qp, ep)
// 发送队列下标递增
c.sendx++
// 发送队列满之后,将指针指回头部,形成环
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 发送数据计算器递增
c.qcount++
// 释放锁
unlock(&c.lock)
return true
}
阻塞发送
当channel
处于open
状态,但是没有接收者,并且没有buf
缓冲队列或者buf
队列已满,这时 channel 会进入阻塞发送。即获取当前channel
发送逻辑所在的goroutine
,并初始化相关的信号量将其阻塞,直到条件满足的情况下唤醒当前goroutine
。
gp := getg()
//从p拿一个sudog结构
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
//设置在等等gp是哪个.在recv的时候,会被重新加入到runq中
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
//把这个结果加入到sendwaiter队列中
c.sendq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
//挂起当前g
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 确保发送的数据在被接受复制之前不会被GC掉
KeepAlive(ep)
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
//归还当前mysg
releaseSudog(mysg)
//在消费端close chan可能会引起panic
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
在select
中,多个case
尝试从一个channel
中获取数据时,会判断当前goroutine
是否被处理
// waitq
func (q *waitq) dequeue() *sudog {
for {
sgp := q.first
if sgp == nil {
return nil
}
y := sgp.next
if y == nil {
q.first = nil
q.last = nil
} else {
y.prev = nil
q.first = y
sgp.next = nil // mark as removed (see dequeueSudog)
}
// 是否已被其他chan处理
if sgp.isSelect && !atomic.Cas(&sgp.g.selectDone, 0, 1) {
continue
}
return sgp
}
}
简单流程图如下:
从Channel接受消息
从channel接受消息的逻辑与发送消息类似,大体逻辑可以概括为以下几点:
- 当channel为nil时,挂起当前
goroutine
- 当channel已经关闭时且channel中没有数据,直接返回
- 当存在挂起的发送者时,通过
recv
从阻塞的发送者或者缓冲区中获取数据; - 当缓冲区存在数据时,直接从
channel
的缓冲区中接收数据; - 当缓冲区中不存在数据且不存在等待的
goroutine
时,从管道中接收数据的操作会变成阻塞的,等待其他goroutine
向channel
发送数据;
同步接收
尝试在channel
的sendq队列中获取等待发送的goroutine
。取出队头等待的 goroutine
。
- 如果缓冲区的大小为 0,则直接从发送方接收值。
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
- 否则,对应缓冲区满的情况,从队列的头部接收数据,发送者的值添加到队列的末尾(此时队列已满,因此两者都映射到缓冲区中的同一个下标)。
同步接收的核心逻辑见下面 recv() 函数:
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// channel中没有缓冲的数据
if c.dataqsiz == 0 {
if raceenabled {
racesync(c, sg)
}
if ep != nil {
// 将sender复制到当前goroutine的堆栈中
recvDirect(c.elemtype, sg, ep)
}
} else {
// Queue is full. Take the item at the
// head of the queue. Make the sender enqueue
// its item at the tail of the queue. Since the
// queue is full, those are both the same slot.
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
}
// copy data from queue to receiver
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
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()
}
goready(gp, skip+1)
}
异步接收
如果 channel
的缓冲区中包含一些数据时,从 channel
中接收数据会直接从缓冲区中 recvx
的索引位置中取出数据进行处理
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
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
}
阻塞接收
封装当前goroutine
上下文,状态设置为等待,加入recvq
队列。调用gopark
方法挂起当前 goroutine
,状态为waitReasonChanReceive
,阻塞等待channel
。
gp := getg() // 调用 getg() 方法获取当前 goroutine 的指针,用于绑定给一个 sudog
mysg := acquireSudog() // 调用 acquireSudog() 方法获取一个 sudog,可能是新建的 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) // 调用 c.recvq.enqueue 方法将配置好的 sudog 加入待发送的等待队列
atomic.Store8(&gp.parkingOnChan, 1)
// 挂起goroutine
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// ...
简单流程图如下:
关闭channel
当调用close
方法关闭channel
时,核心流程如下:
- 安全检查
// 关闭未初始化的channel
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
// 关闭已经关闭的channel
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
- 释放所有读写
channel
的g
并加入待清除队列glist
var glist gList
// release all readers
for {
// ...
glist.push(gp)
}
// release all writers (they will panic)
for {
// ...
glist.push(gp)
}
- 最后会为所有被阻塞的
g
调用goready
触发调度。将所有glist
中的g
状态从_Gwaiting
设置为_Grunnable
状态,等待调度器的调度。
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
正确关闭channel
Channel | Status | Result |
---|---|---|
close | nil | panic |
close | 打开且非空 | 关闭 Channel;读取成功,直到 Channel 耗尽数据,然后读取产生值的默认值 |
close | 打开但为空 | 关闭 Channel;读到生产者的默认值 |
close | 关闭 | panic |
close | 只读 | Compile Error |