持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第28天,点击查看活动详情
3.3 、channel 发送元素
当我们想要向 Channel 发送数据时,就需要使用 ch <- i 语句,编译器会将该语句最终转化为 runtime.chansend 的调用,如果我们在调用时将 block 参数设置成 true,那么表示当前发送操作是阻塞的
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ...
}
因为 runtime.chansend 函数的实现比较复杂,所以我们这里将该函数的执行过程分成以下的四个部分:
- 基础校验,获取锁
- 当存在等待的接收者时,通过 runtime.send 直接将数据发送给阻塞的接收者;
- 当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
- 当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据;
3.3.1、基础校验
如果Channel 未初始化,发送数据时会让出当前协程或者直接返回失败; 对非阻塞、未关闭并且 buf已满或者chan属于无缓冲的Channel 进行发送数据时,直接返回失败 基础校验完毕后首先会对当前channel 加锁,防止多个线程并发修改数据。如果 Channel 已经关闭,那么向该 Channel 发送数据时会报 “send on closed channel” 错误并中止程序
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if !block && c.closed == 0 && full(c) {
return false
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
3.3.2、直接发送
如果目标 Channel 没有被关闭并且已经有处于读等待的 Goroutine,那么 runtime.chansend 会从接收队列 recvq 中取出最先陷入等待的 Goroutine 并直接向它发送数据: 对于无缓冲的channel,如果存在读取的协程,必然会挂载到recvq队列上
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
}
对于send 方法,可以分为两个部分:
- 调用 runtime.sendDirect 将发送的数据直接拷贝到 x = <-c 表达式中变量 x 所在的内存地址上;
- 调用 runtime.goready 将等待接收数据的 Goroutine 标记成可运行状态 Grunnable 并把该 Goroutine 放到发送方所在的处理器的 runnext 上等待执行,该处理器在下一次调度时会立刻唤醒数据的接收方;
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()
}
// 将接收方的 Goroutine 放到了处理器的 runnext 中,程序没有立刻执行该 Goroutine。
goready(gp, skip+1)
}