Go 中 channel 中用法和实现总结
以下分析和源码都是基于 go1.17 版本
channel 简介
Go 语言的基础类型之一, 用于在协程与协程之间传递数据 (channel 数据的传输方式也是值传递, Go语言的数据传输只有值传递)
Do not communicate by sharing memory; instead, share memory by communicating.
channel 保证:
- 数据的先入先出
- 并发情况下的数据安全
- 已经关闭的 channel 不可重开
channel 的实现
channel 在内部实现的结构体为 runtime.hchan
- 有一个环形链表, 暂存要传输的数据. 无 buffer 的channel 该队列长度为0, 所以不进行数据缓冲.
- 有一把互斥锁
mutex
, 在并发情况下, 保护自身数据结构的一致性 - 有两个协程等待链表, 用于挂载因为发送/接收而阻塞在该 channel 上的协程
|
|
初始化
- channel 传递的元素不能太大
- 如果是空结构体或者无缓冲队列, 是不需要分配环形队列内存
- 如果传递数据类型有内含指针, 需要将环形队列分配到堆上
内部实现函数runtime.makechan
|
|
关闭
核心代码runtime.closechan
更新自身数据结构中的关闭状态, 并 唤醒阻塞在 channel 上的所有协程. 被唤醒的协程(sudog
)的 success 标识会被置为 false.
被唤醒的 写操作的协程, 也会发生panic. ( “send on closed channel” )
自身操作会发生 panic 的情况
- 未初始化 channel
- 重复关闭 channel
|
|
发送数据
向已经关闭的 channel 发送数据会发生 panic
数据流程:
- 检查是否已经初始化
- 非阻塞写入数据, 检查数据是否已经满, 快速返回
- 是否已经关闭
- 检查 channel 中是否已经有等待获取数据而阻塞的协程, 如果有直接将数据发送给等待的协程.
- channel 的 buffer 是否还有空间, 如果有将数据放置到 buffer 中, 返回
- channel 的 buffer 已经满了, 根据是否为 select 操作, 判断是否需要将协程阻塞
- 当协程阻塞之后, 在被唤醒之后需要再检查一次, channel 是否已经关闭.
|
|
接收数据
与发送数据一样, 同样带着是否阻塞的参数. 在编译时,由是否有 select 操作决定. 核心代码runtime.chanrecv
- 不带 select 从未初始化的 channel 获取数据, 会永远阻塞
runtime.chanrecv
返回值中, 第一个返回值selected
表示在,select 语句中, 该 case 是否会被选中执行
接收数据流程:
- 检查是否已经初始化
- 检查非阻塞获取数据下, 是否可以直接返回
- 如果已经关闭的 channel 且没有已经没有缓冲数据, 返回数据类型的默认值.
- 检查有因发送数据阻塞在 channel 的协程, 如果没有 buffer, 直接从阻塞的协程中获取数据, 否则从 buffer 中获取数据数据, 将第一个阻塞的协程的数据放入 buffer 中.
- 如果缓冲 buffer 有数据, 则从buffer 中获取数据.
- 非阻塞操作, 直接返回. 否则协程进行阻塞.
注意事项:
当 select 一个 已经关闭的 channel 的时候, 该 case 会被疯狂输出, 导致cpu使用率上升
|
|
用法总结
初始化:
- 避免对未初始化 channel 的进行读写操作, 可能会造成阻塞
- 在 select 语句中, 对已经关闭的 channel 可以赋予
nil
值, 避免 cpu 飙高
关闭协程:
- 关闭协程的动作, 应该由数据写入方操作
- channel 当参数传递时, 尽可能带上操作方向(读取/写入), 编译器会保证, 单向写入协程不允许关闭
- 关闭的时候要确保所有的写入协程都已经操作完毕. 避免引起写入协程发生 panic
在 channel 中阻塞的协程, 唤醒条件
- 到达协程数据操作的目标, 写入 / 读取数据
- channel 关闭