channel
数据结构
Channel 的数据结构定义在 runtime/chan.go 中的 hchan 结构体中
- hchan 结构体包含以下字段:
type hchan struct { qcount uint // 队列中当前元素个数 dataqsiz uint // 循环队列的长度 buf unsafe.Pointer // 指向循环队列的指针 elemsize uint16 // 元素大小 closed uint32 // 标识 channel 是否已关闭 elemtype *_type // 元素类型 sendx uint // 下一个发送元素在 buf 中的索引 recvx uint // 下一个接收元素在 buf 中的索引 recvq waitq // 等待接收数据的 goroutine 队列(双向链表) sendq waitq // 等待发送数据的 goroutine 队列(双向链表) lock mutex // 保护 hchan 的互斥锁 } - buf 是一个循环队列,用于存储 channel 接收的数据 sendx 和 recvx 分别记录下一个可以发送和接收的元素在 buf 中的索引位置
- recvq 和 sendq 是两个双向链表,分别用于存储等待从 channel 接收数据和等待向 channel 发送数据的 goroutine lock 是一个互斥锁,用于保护 hchan 结构体中的所有字段,确保 channel 的线程安全性
- elemsize 和 elemtype 分别记录 channel 中元素的大小和类型,用于数据的拷贝和类型检查
- closed 标识 channel 是否已关闭,用于在发送和接收操作时进行检查
底层原理
Channel 本质上是由三个 FIFO(先进先出)队列组成的用于协程之间传输数据的协程安全的通道 这三个队列分别是: buf 循环队列:存放 channel 接收的数据 sendq 待发送者队列:存放等待发送数据到 channel 的 goroutine 的双向链表 recvq 待接收者队列:存放等待从 channel 接收数据的 goroutine 的双向链表
Channel 的工作原理
- 对于无缓冲的 channel,如果没有接收者准备好,发送操作会阻塞发送者 goroutine;如果没有发送者准备好,接收操作会阻塞接收者 goroutine
- 对于有缓冲的 channel,在缓冲区未满时发送操作不会阻塞,在缓冲区非空时接收操作不会阻塞。数据先写入缓冲区 buf,然后从缓冲区读取
- Channel 传递数据的本质是值拷贝,包括引用类型数据的地址拷贝。数据从发送者栈内存拷贝到缓冲区 buf,或从缓冲区 buf 拷贝到接收者栈内存
- 关闭一个 channel 会导致 panic,除非是向一个已经关闭的 channel 发送数据。关闭 channel 后,后续的发送操作会 panic,而接收操作会返回对应类型的零值和 false
- 使用无缓冲 channel 时,如果发送者和接收者不能及时配对,可能会导致 goroutine 泄漏,造成内存泄漏。可以通过使用有缓冲 channel 或在发送后关闭 channel 来避免这个问题
综上所述,Golang 的 channel 机制是基于环形队列和双向链表实现的,设计上体现了 Go 并发编程的理念"不要通过共享内存来通信,而是通过通信来共享内存"。开发者需要谨慎使用 channel,特别是无缓冲 channel,以避免出现 goroutine 泄漏等问题。
| 操作 | 通道状态 | 结果 | 解释 |
|---|---|---|---|
| 写入到 Channel | 未初始化 | panic | 向未初始化的通道写入数据将导致 panic |
| 未关闭 | 成功/阻塞 | 如果有足够的空间,写入将成功并继续运行;如果通道已满,写入操作将阻塞直到有空间或者通道被关闭;如果通道被关闭,将会 panic | |
| 已关闭 | panic | 写入操作将 panic | |
| 读取 Channel | 未初始化 | 阻塞 | 从未初始化的通道读取数据将会永远阻塞,直到通道被初始化为止 |
| 未关闭 | 成功/阻塞 | 如果有值,读取将成功并返回该值;如果没有值,读取操作将阻塞直到有值或者通道被关闭;如果通道被关闭且为空,返回默认值 | |
| 已关闭 | 默认值 | 如果有值,读取将成功并返回该值;如果没有值,立即返回默认值;如果通道被关闭且为空,返回默认值 | |
| 关闭 Channel | 未初始化 | 尝试关闭未初始化的通道将导致 panic | |
| 未关闭 | 关闭通道,后续对通道的读取操作将返回默认值 | ||
| 已关闭 | panic | 关闭通道将 panic |