上集回顾:channel创建/关闭
1.发送
为了你有更好的阅读体验,建议你先观看 channel结构体
我们需要有一个认知,接收方阻塞,其中缓冲区和sendq队列一定是空的。非阻塞状态,一是缓冲区未满,二是缓冲区已满,将发送方加入到sendq队列。
1.1 校验
校验部分你能看到如果非阻塞状态遇到未初始化、关闭的、缓冲区满等条件都会直接退出,返回false。
其中需要注意的是不能向已关闭的channel发送数据,会异常退出的
最后所有写入的数据都是需要加锁的,channel并发的保证。
还有一些额外的cpu时钟周期和竞态检测器,你会在这里找到答案:CPU时钟周期,以及竞态检测器
/*
:params c: hchan实例
:params ep: 指向发送缓冲区的指针
:params block: 是否加锁
:params cellerpc: 返回其调用者的程序计数器(PC)
*/
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
if !block {
return false
}
// chanenl为空,channel无法发送数据,将G挂起,异常退出
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
}
// 一个已经关闭的channel是不可能从“准备发送”的状态变成“未准备好发送”的状态。
// 所以在检查完channel是否关闭以后,就算channel关闭了,也不影响此处检查的结果
if !block && c.closed == 0 && full(c) {
return false
}
var t0 int64
if blockprofilerate > 0 {
// 获取CPU时钟周期
t0 = cputicks()
}
// 锁住 channel,并发安全
lock(&c.lock)
// 不能向一个已经close的channel发送数据
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
...
}
1.2 存在阻塞接收方且新增发送方
如果存在阻塞接收方,证明缓冲区是空的,sendq也是空的。所以新增的发送方,可以先直接找recvq队列进行匹配。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
if sg := c.recvq.dequeue(); sg != nil {
// 找到一个等待的接收者,我们将要发送的值传递给接收器,绕过信道缓冲区(如果有的话)
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
...
}
1.3 缓冲区未满
在缓冲区未满的时候,可以将元素放到缓冲区,并将发送指针+1。也许你已经注意到当,没错,缓冲区也被定义为环形队列。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 2.缓冲区:当缓冲区未满,那么通过移动环形队列缓存的指针来存储消息
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++
// 循环队列,如果发送游标值等于容量值,游标值归0
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 队列元素数量+1
c.qcount++
// 解锁
unlock(&c.lock)
return true
}
...
}
1.4 缓冲区已满且仍有发送方(阻塞)
因为缓冲区已经满的缘故,我们还需要一个队列可以进行对元素的存放,将元素进行sudog结构体针对当前G的封装,然后存入 sendq的双向队列中。最后就是将当前G让出执行权
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 由于还没有接收者,如果程序不阻塞,将直接退出
if !block {
unlock(&c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
// 对通道加锁,某个接收器将为我们完成操作(通过sudog封装当前的G,并放入sendq双向链表)
// 获取当前g
gp := getg()
// 调用acquireSudog()方法获取一个sudog,可能是新建的sudog,也有可能是从缓存中获取的。设置好sudog要发送的数据和状态。
// 比如发送的channel、是否在select中和待发送数据的内存地址等等。
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 在分配elem和在 go.waiting上排队的mysg之间没有堆栈分割,在copystack可以找到它
mysg.elem = ep // ch <- ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 将配置好的 sudog 加入待发送的等待队列
c.sendq.enqueue(mysg)
...
}
1.5 goroutine进入休眠状态
实现 Goroutine 的等待功能,通过 park() 函数使 Goroutine 进入休眠状态等待 channel 操作完成。避免了 Goroutine 固定时间间隔内(超时机制)不停地查询 channel 状态,从而尽可能地减少了 Goroutine 的 CPU 占用率。
atomic.Store8 使用了原子操作,将一个字节设为非0值。 其可以将一个标志设置为当前goroutine正在通道操作中被阻塞。
gopark() 函操作将当前的goroutine暂停并释放它的g标识符(goid)。只有在通道操作被执行或取消时,该goroutine才会再次恢复并重新启动。
KeepAlive:确保ep所引用的对象在此函数返回之前不被垃圾回收器回收。这是为了避免在发送操作期间发生垃圾回收,导致数据丢失。
综上所述,该代码段的作用是将一个goroutine加入到等待发送的队列中,并暂停该goroutine,直到channel有数据可用或者channel关闭。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
omic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
KeepAlive(ep)
}
gopark
| 字段 | 对应值 | 含义 |
|---|---|---|
| unlockf | chanparkcommit | 一个函数指针,表示将当前的 goroutine 添加到等待列表中,并且等待另一个 goroutine 完成相应的操作后再将这个 goroutine 唤醒。 |
| lock | unsafe.Pointer(&c.lock) | 是 channel 的锁,表示用于挂起/恢复 channel 的读写操作以及其它的同步操作。在这里,c 是一个指向 hchan 结构体的指针,表示一个无缓冲、双向的 channel 的描述符。 |
| reason | waitReasonChanSend | 表示当前 goroutine 阻塞的原因是正在发送数据到 channel。 |
| traceEv | traceEvGoBlockSend | 表示对当前操作进行跟踪和记录,可以用于运行时调试和性能分析。 |
| traceskip | 2 | 表示当前 goroutine 应该被挂起,直到有其它 goroutine 从 channel 中接收到数据,并将这个 goroutine 从等待列表中唤醒,以继续发送操作。 |
KeepAlive
当前goroutine没有被唤醒,那么在KeepAlive()函数调用结束之后,如果ep所引用的对象已经不再被任何值所引用,那么这个对象就会被垃圾回收器回收。
但是在等待期间(在调用park()和gopark()之间), ep所引用的对象至少被一个值所引用,也就是被goroutine所引用,因此不会被垃圾回收器回收。
当某些事件(如channel被关闭)发生时,其他的goroutine将被唤醒并从等待队列中移除,当被唤醒的goroutine重新获取运行时,其内部的代码会继续执行下去。在这种场景下,KeepAlive()函数的作用已经得到了充分保证。
总之,Go语言运行时系统会根据需要来唤醒阻塞的goroutine,保证阻塞期间所引用的对象不会被回收,从而确保程序的正确性。
1.6 goroutine唤醒
在channel中的g被唤醒有两种
- 当接收者成功从channel中读取到数据时,它会将这个值返回给调用者,并唤醒等待在该channel上的goroutine继续执行。
- channel关闭后,会唤醒所有的等待队列(发送者/接收者)
无论是哪一种情况都需要归还sudog对象。sudog对象是goroutine和channel之间的中介对象,它会在挂起goroutine的时候被创建并加入等待队列,然后唤醒goroutine的时候归还,这个过程确保了sudog对象的有效重用,避免了频繁的创建和销毁对象的开销。
在最后当唤醒的goroutine处于channel关闭状态,这就意味着是向一个关闭chanenl发数据,直接异常处理。所以在日常对channel的关闭,最好要在发送端来掌控。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// sudog算是对g的一种封装,里面包含了g,要发送的数据以及相关的状态。
// goroutine被唤醒后会完成channel的阻塞数据发送。发送完最后进行基本的参数检查,解除channel的绑定并释放sudog
// someone woke us up.
// 当goroutine唤醒以后,解除阻塞状态
if mysg != gp.waiting {
// G等待队列已经损坏
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)
// 当前goroutine被唤醒后,如果发现channel已经被关闭了,则panic(也就是说不能向已经关闭的channel发送数据)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}
2.接收
2.1 校验
校验有两种, 一是非阻塞状态:chanenl是nil或者空,都可以直接退出了,最后将ep清空。 二是阻塞状态:channel是关闭了,可以直接退出了,最后将ep清空。
还有一些额外的cpu时钟周期和竞态检测器,你会在这里找到答案:CPU时钟周期,以及竞态检测器
/*
:params c: hchan结构体
:params ep: 指向接收缓冲区的指针
:params block: 是否阻塞
:return selected,received: select关键字返回的结果,接收者返回的结果
*/
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// Raceenabled:不需要检查ep,因为它总是在堆栈上,或者是由reflect分配的新内存。
if debugChan {
print("chanrecv: chan=", c, "\n")
}
// 从一个nil的channel接收数据将会永远阻塞,异常退出
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 快速路径:在未获取锁的情况下检查失败的非阻塞操作
if !block && empty(c) {
// 原子操作,读取c.closed变量的值
if atomic.Load(&c.closed) == 0 {
// channel的关闭时不可逆的,所以能通过这种方式判定退出(空数据且通道关闭)
return
}
if empty(c) {
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)
if c.closed != 0 {
// 通道已关闭且没数据
if c.qcount == 0 {
// 通道不存在元素,直接收尾
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
// 根据类型清理相应地址的内存
typedmemclr(c.elemtype, ep)
}
// 从已关闭且没数据的 channel 接收,selected 会返回true,received返回 false
return true, false
}
} else {
...
}
...
2.2 存在阻塞发送方并新增接收方
如果存在阻塞发送方,证明缓冲区是满的,并且recvq是空的,所以新增的接收方,可以直接找recvq队列进行匹配。
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
if c.closed != 0 {
...
} else {
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
}
...
}
2.3 缓冲区未满
当缓冲区未满的时候,可以将接收指针所在的元素赋值给接收者,然后移除缓冲区元素,并将数量-1。
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
if c.qcount > 0 {
// 直接从队列中接收
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
}
// 队列元素数量-1
c.qcount--
unlock(&c.lock)
return true, true
}
if !block {
unlock(&c.lock)
return false, false
}
...
}
2.4 缓冲区已空且仍有接收方
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 通过sudog封装当前G,并加入到recvq双向链表中
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 在分配elem和在gp上排队mysg之间没有堆栈割裂。在拷贝堆能找到的地方等待
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// 加入recvq等待队列
c.recvq.enqueue(mysg)
...
}
2.5 goroutine进入休眠状态
关于下边的信息参考:章节(1.5)
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
...
gopark
| 字段 | 对应值 | 含义 |
|---|---|---|
| unlockf | chanparkcommit | 一个函数指针,表示将当前的 goroutine 添加到等待列表中,并且等待另一个 goroutine 完成相应的操作后再将这个 goroutine 唤醒。 |
| lock | unsafe.Pointer(&c.lock) | 是 channel 的锁,表示用于挂起/恢复 channel 的读写操作以及其它的同步操作。在这里,c 是一个指向 hchan 结构体的指针,表示一个无缓冲、双向的 channel 的描述符。 |
| reason | waitReasonChanReceive | 表示当前 goroutine 阻塞的原因是channel正在接收 |
| traceEv | traceEvGoBlockRecv | 表示对当前操作进行跟踪和记录,可以用于运行时调试和性能分析。 |
| traceskip | 2 | 表示当前 goroutine 应该被挂起,直到有其它 goroutine 从 channel 中接收到数据,并将这个 goroutine 从等待列表中唤醒,以继续发送操作。 |
2.5 唤醒
同样,当发送者成功将数据发送到channel时,它会将这个值返回给调用者,goroutine被唤醒后会完成channel的阻塞数据接收.接收完最后进行基本的参数检查,解除channel的绑定并释放sudog。
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
//
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
}
3.公共函数
3.1 数据发送
这个函数将发送数据到一个通道,并调度等待数据的协程。同时,如果启用了竞争检查,则会进行数据竞争监视。
在发送操作中,发送者将要发送的值ep被复制到接收者sg中,然后接收者会被唤醒并能够继续运行。
在执行发送操作之前,通道c必须是空的并且已被锁定。发送完毕后,通道c将被解锁并使用unlockf进行解锁。
同时,接收者的sudog必须已经从该通道中出队,并且发送的值ep必须是非空的,指向堆或调用者的堆栈。
/*
:params c: 要接收数据的channel
:params sg: 当前select 或 go语句对应的 sudog结构体
:params ep: 接收到的数据要存储的位置
:params unlockf: 用于解锁channel,通常是一个函数类型
:params skip: 表示是否跳过少量的接收操作(因为channel中可能同时存在多个接收者)
*/
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if raceenabled {
if c.dataqsiz == 0 {
racesync(c, sg)
} else {
// 虽然我们直接复制,但假装我们经历了缓冲区。需要注意的是,只有在启用了"raceenabled"情况下才需要增加头/尾位置。
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
}
if sg.elem != nil {
// 将要发送的数据ep复制到接收者sg中(直接拷贝内存(从发送者到接收者))
sg.elem = nil
sendDirect(c.elemtype, sg, ep)
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 将G重新放入处理器p的本地运行队列,等待被调度
goready(gp, skip+1)
}
3.2 数据接收
当通道已满时,接收任务(recv)在通道上执行接收操作。包括以下两个部分:
- 将发送者(sg)发送的值放入通道中,唤醒发送者并让其继续执行。
- 将接收者(当前的G)接收到的值写入指针变量
ep所指向的位置。
对于同步通道,这两个值是相同的;对于异步通道,接收者从通道缓冲区获取数据,而发送者的数据则被放入通道缓冲区中。在执行接收操作时,需要确保通道已满且已被锁定,此时使用解锁函数(unlockf)解锁通道。接收时,指针变量ep必须非空,且必须指向堆或调用者的堆栈上的位置。
/*
:params c: 要接收数据的channel
:params sg: 当前select 或 go语句对应的 sudog结构体
:params ep: 接收到的数据要存储的位置
:params unlockf: 用于解锁channel,通常是一个函数类型
:params skip: 表示是否跳过少量的接收操作(因为channel中可能同时存在多个接收者)
*/
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.dataqsiz == 0 {
if raceenabled {
racesync(c, sg)
}
if ep != nil {
// copy data from sender
// 从 sender里面拷贝数据
recvDirect(c.elemtype, sg, ep)
}
} else {
// 如果队列已经满了,就需要将队列头部的元素弹出,让发送者将它的元素放到队列尾部。
// 因为队列已经满了,所以这两个位置是相同的。
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
}
// 将数据从buf中拷贝到接收者内存地址中
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 将数据从sendq中拷贝到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()
}
goready(gp, skip+1)
}