08.Golang 通道(channel )源码分析(二、数据发送与接收)
Golang 通道(channel )源码分析(二、数据发送与接收)
注意当前go版本代码为1.23
Channel 在运行时的内部表示是
runtime.hchan
介绍
当向 Channel 发送数据时,就需要使用 ch <- i
语句,编译器会将它解析成 OSEND
节点并在 cmd/compile/internal/gc.walkexpr
中转换成
而接收 Channel数据时,可以使用两种不同的方式去接收 Channel 中的数据:
1 |
|
最终会通过编译器转换成 runtime.chanrecv1
和 runtime.chanrecv2
两种不同函数的调用,这两个函数最终会调用 runtime.chanrecv
进行处理。
数据发送
1 |
|
从上述代码可以知道,go通道发送主要流程总结如下:
发送流程:
- 空 Channel 检查 && 竞态检测 && (非阻塞)快速检测 && 加锁 && Channel 关闭检查:
- 三种发送情况:
- 情况 1: 存在等待的接收者 (
c.recvq.dequeue() != nil
)- 从接收队列
c.recvq
中取出一个等待的接收者sg
。 - 调用
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
直接将数据从发送者ep
传递给接收者sg
,绕过缓冲区。 - 发送成功后唤醒接收者
sg
。 - 发送成功,返回
true
。
- 从接收队列
- 情况 2: 缓冲区有空间 (
c.qcount < c.dataqsiz
)- 计算下一个可用缓冲区位置
qp
。 - 如果启用了竞态检测,调用
racenotify
通知将要向缓冲区写入数据。 - 使用
typedmemmove
将数据从ep
复制到缓冲区qp
。 - 更新发送索引
c.sendx
和缓冲区元素计数c.qcount
。 - 释放锁
c.lock
。 - 发送成功,返回
true
。
- 计算下一个可用缓冲区位置
- 情况 3: 不存在缓冲区或者缓冲区已满且为阻塞模式 (
block == true
)- 获取当前 goroutine 的指针
gp
。 - 从
sudog
池中分配一个sudog
结构体mysg
。 - 初始化
mysg
的各个字段,包括要发送的数据指针ep
、当前 goroutinegp
、当前 channelc
等。 - 将
mysg
加入到 channel 的发送队列c.sendq
中。 - 设置
gp.parkingOnChan
为true
,表示 goroutine 即将因 channel 操作阻塞。 - 调用
gopark
阻塞当前 goroutine,等待被唤醒。 gopark
会释放锁c.lock
。- 被唤醒后,检查
gp.waiting
是否仍然指向mysg
,确保等待列表的完整性。 - 检查
mysg.success
字段,判断发送是否成功。 - 清理
mysg
并释放。 - 如果
closed
为true
(发送失败),且 channel 未关闭,则抛出 “spurious wakeup” 异常 - 返回
(true, success)
。
- 获取当前 goroutine 的指针
- 情况 1: 存在等待的接收者 (
数据接收
1 |
|
主要逻辑和情况处理:
- 空 Channel 检查 && 竞态检测 && (非阻塞)快速检测 && 加锁 && Channel 关闭检查:
- 通道未关闭:
- 情况 1: 存在等待的发送者 (
c.sendq
不为空):- 调用recv函数接收数据,如果 Channel 不存在缓冲区,调用 runtime.recvDirect 将 Channel 发送队列中 Goroutine 存储的 elem 数据拷贝到目标内存地址中;如果 Channel 存在缓冲区;将发送队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;
- 情况 2: 缓冲区中有数据 (
c.qcount > 0
):- 直接从缓冲区
c.recvx
索引处读取数据。 - 如果
ep
不为nil
,则将数据拷贝到ep
指向的内存。 - 更新
c.recvx
和c.qcount
。 - 返回
(true, true)
。
- 直接从缓冲区
- 情况 3: 不存在缓冲区或者缓冲区为空且为阻塞模式:
- 获取当前 goroutine 的指针
gp
。 - 从
sudog
池中分配一个sudog
结构体mysg
。 - 初始化
mysg
的各个字段,包括要发送的数据指针ep
、当前 goroutinegp
、当前 channelc
等。 - 将
mysg
加入到 channel 的发送队列c.recvq
中。 - 调用
gopark
函数阻塞当前 goroutine,等待被唤醒。 - 被唤醒后,检查
gp.waiting
是否仍然指向mysg
,确保等待列表的完整性。 - 检查
mysg.success
字段,判断发送是否成功。 - 清理
mysg
并释放。 - 返回
(true, success)
。
- 获取当前 goroutine 的指针
- 情况 1: 存在等待的发送者 (
问题
操作 | nil channel | closed channel | not nil, not closed channel |
---|---|---|---|
close | panic | panic | 正常关闭 |
读 <- ch | 阻塞 | 读到对应类型的零值 | 阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞 |
写 ch <- | 阻塞 | panic | 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞 |
1.Channel 发送和接收数据的本质是什么?
All transfer of value on the go channels happens with the copy of value.
就是说 channel 的发送和接收操作本质上都是 “值的拷贝”,无论是从 sender goroutine 的栈到 chan buf,还是从 chan buf 到 receiver goroutine,或者是直接从 sender goroutine 到 receiver goroutine。
1 |
|
1 |
|
参考链接
2.《Go学习笔记》
08.Golang 通道(channel )源码分析(二、数据发送与接收)
https://blog.longpi1.com/2024/12/14/08-Golang-通道(channel-)源码分析(二、数据发送与接收)/