231 lines
7.6 KiB
Markdown
231 lines
7.6 KiB
Markdown
---
|
||
tags: [go, golang, go-principle, channel]
|
||
create time: 2026-06-07 15:25
|
||
---
|
||
|
||
# Channel 底层原理
|
||
|
||
## 概述
|
||
|
||
本文从 runtime 源码角度深入解析 Go Channel 的数据结构、send/recv 全流程,以及无缓冲和有缓冲 channel 的行为差异。Channel 是 Go CSP 并发模型的基石,理解其底层实现能帮你正确设计并发程序、避免死锁。
|
||
|
||
> [!question] ❓ 思考
|
||
> 为什么向关闭的 channel 发送数据会 panic,但从关闭的 channel 读取数据不会?当 sendq 和 recvq 同时有等待者时,数据是直接 goroutine 间传递还是经过缓冲区?
|
||
|
||
## 正文
|
||
|
||
### 一、Channel 的数据结构:hchan
|
||
|
||
Channel 在 runtime 中用 `hchan` 结构体表示:
|
||
|
||
```go
|
||
// src/runtime/chan.go
|
||
type hchan struct {
|
||
qcount uint // 队列中当前元素个数
|
||
dataqsiz uint // 环形缓冲区大小
|
||
buf unsafe.Pointer // 环形缓冲区指针
|
||
elemsize uint16 // 每个元素的大小
|
||
closed uint32 // 是否已关闭
|
||
elemtype *_type // 元素类型
|
||
sendx uint // 发送索引(环形)
|
||
recvx uint // 接收索引(环形)
|
||
recvq waitq // 等待接收的 goroutine 链表
|
||
sendq waitq // 等待发送的 goroutine 链表
|
||
lock mutex // 保护所有字段的互斥锁
|
||
}
|
||
```
|
||
|
||
用一个有缓冲 channel(buf=8, 已有4个元素)来描述:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
H["hchan"] --> Q["qcount=4"]
|
||
H --> D["dataqsiz=8"]
|
||
H --> B["buf: [100][200][300][400][ ][ ][ ][ ]"]
|
||
H --> S["sendx=4"]
|
||
H --> R["recvx=0"]
|
||
H --> SQ["sendq: G1→G2→nil"]
|
||
H --> RQ["recvq: nil"]
|
||
style B fill:#fff9c4
|
||
style SQ fill:#ffebee
|
||
style RQ fill:#e8f5e9
|
||
```
|
||
|
||
#### Waitq 与 Sudog
|
||
|
||
```go
|
||
type waitq struct {
|
||
first *sudog
|
||
last *sudog
|
||
}
|
||
|
||
type sudog struct {
|
||
g *g // 绑定的 goroutine
|
||
elem unsafe.Pointer // 传递的数据
|
||
c *hchan // 所属 channel
|
||
next *sudog
|
||
prev *sudog
|
||
success bool // 操作是否成功
|
||
// ...
|
||
}
|
||
```
|
||
|
||
`waitq` 是一个双向链表,存储的是 `sudog` 而非 `goroutine`——因为 `sudog` 额外携带了数据地址和成功标志。
|
||
|
||
### 二、Channel 初始化:makechan
|
||
|
||
```go
|
||
func makechan(t *chantype, size int) *hchan {
|
||
mem := elem.size * uintptr(size)
|
||
var c *hchan
|
||
switch {
|
||
case mem == 0:
|
||
// 无缓冲 channel:只分配 hchan
|
||
c = (*hchan)(mallocgc(hchanSize, nil, true))
|
||
case elem.ptrdata == 0:
|
||
// 无指针元素:一次分配 hchan + buf
|
||
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
|
||
c.buf = add(unsafe.Pointer(c), hchanSize)
|
||
default:
|
||
// 有指针元素:分开分配
|
||
c = new(hchan)
|
||
c.buf = mallocgc(mem, elem, true)
|
||
}
|
||
c.dataqsiz = uint(size)
|
||
lockInit(&c.lock, lockRankHchan)
|
||
return c
|
||
}
|
||
```
|
||
|
||
> [!note] 📝 源码要点
|
||
> 对于不含指针的小元素类型(如 `int`),Go 会把 hchan 和 buf 分配到同一片连续内存中,减少一次 malloc 开销。含指针的类型必须分开分配,否则 GC 无法正确扫描 buf 区域。
|
||
|
||
### 三、Send 操作流程
|
||
|
||
```go
|
||
ch <- value // 编译期转换为 chansend(ch, &value, true, getcallerpc())
|
||
```
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start["chansend 开始"] --> NilCh{"ch == nil?"}
|
||
NilCh -->|是| ParkNil["gopark 永久挂起"]
|
||
NilCh -->|否| Closed{"channel 已关闭?"}
|
||
Closed -->|是| Panic["panic: send on closed channel"]
|
||
Closed -->|否| Lock["加锁"]
|
||
Lock --> RecvWait{"recvq 有等待者?"}
|
||
RecvWait -->|是| DirectTransfer["直接 G→G 传递<br/>绕过缓冲区"]
|
||
RecvWait -->|否| SpaceAvail{"buf 有空位?"}
|
||
SpaceAvail -->|是| BufferCopy["拷贝到 buf<br/>sendx++, qcount++"]
|
||
SpaceAvail -->|否| NoSpace{非阻塞?"}
|
||
NoSpace -->|是| ReturnFalse["返回 false"]
|
||
NoSpace -->|否| CreateSudog["创建 sudog, 加入 sendq"]
|
||
CreateSudog --> ParkChan["gopark 挂起"]
|
||
DirectTransfer --> Unlock["解锁"]
|
||
BufferCopy --> Unlock
|
||
ParkChan --> Woken["被 recv 唤醒后解锁"]
|
||
Unlock --> Done["返回 true"]
|
||
Woken --> Done
|
||
style DirectTransfer fill:#e8f5e9
|
||
style BufferCopy fill:#fff9c4
|
||
style Panic fill:#ffebee
|
||
```
|
||
|
||
关键场景分析:
|
||
|
||
| 场景 | 行为 |
|
||
|------|------|
|
||
| ch == nil | 永远阻塞 |
|
||
| 已关闭 | panic |
|
||
| recvq 有等待者 | **G→G 直接传递**,不经过 buf |
|
||
| buf 有空位 | 拷贝到 buf,立即返回 |
|
||
| buf 满且 recvq 空 | sudog 入队,gopark 挂起 |
|
||
|
||
> [!tip] 💡 理解要点
|
||
> 无缓冲 channel 的 send/recv 本质上是"握手"过程——发送方和接收方同时准备好时,数据直接从发送方的栈拷贝到接收方的栈,完全不经过 hchan 内部。
|
||
|
||
### 四、Recv 操作流程
|
||
|
||
```go
|
||
value := <-ch // 编译期转换为 chanrecv(ch, &value, true)
|
||
```
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start["chanrecv 开始"] --> NilCh{"ch == nil?"}
|
||
NilCh -->|是| ParkNil["gopark 永久挂起"]
|
||
NilCh -->|否| Closed{"closed 且 buf 空?"}
|
||
Closed -->|是| ZeroValue["返回零值, ok=false"]
|
||
Closed -->|否| Lock["加锁"]
|
||
Lock --> SendWait{"sendq 有等待者?"}
|
||
SendWait -->|是| DirectRecv["G→G 直接接收"]
|
||
SendWait -->|否| BufHasData{"buf 有数据?"}
|
||
BufHasData -->|是| BufCopy["从 buf 取出<br/>recvx++, qcount--"]
|
||
BufHasData -->|否| NoData{非阻塞?"}
|
||
NoData -->|是| ReturnFF["返回 false, false"]
|
||
NoData -->|否| CreateSudog["创建 sudog, 加入 recvq"]
|
||
CreateSudog --> ParkChan["gopark 挂起"]
|
||
DirectRecv --> Unlock["解锁"]
|
||
BufCopy --> Unlock
|
||
ParkChan --> Woken["被 send 唤醒后解锁"]
|
||
Unlock --> Done{"ok?"}
|
||
Woken --> Done
|
||
style DirectRecv fill:#e8f5e9
|
||
style BufCopy fill:#fff9c4
|
||
style ZeroValue fill:#fff3e0
|
||
```
|
||
|
||
### 五、Close 流程
|
||
|
||
```go
|
||
close(ch) // 编译期转换为 closechan(ch)
|
||
```
|
||
|
||
```go
|
||
func closechan(c *hchan) {
|
||
if c == nil { panic("close of nil channel") }
|
||
lock(&c.lock)
|
||
if c.closed != 0 { panic("close of closed channel") }
|
||
c.closed = 1
|
||
|
||
var glist gList
|
||
// 1. 唤醒所有 recvq 中的 goroutine(返回零值)
|
||
for { sg := c.recvq.dequeue(); if sg == nil { break }
|
||
sg.elem = nil; sg.success = false; glist.push(sg.g)
|
||
}
|
||
// 2. 唤醒所有 sendq 中的 goroutine(触发 panic)
|
||
for { sg := c.sendq.dequeue(); if sg == nil { break }
|
||
sg.elem = nil; sg.success = false; glist.push(sg.g)
|
||
}
|
||
unlock(&c.lock)
|
||
// 3. 批量恢复调度
|
||
for !glist.empty() { goready(glist.pop(), 3) }
|
||
}
|
||
```
|
||
|
||
> [!warning] ⚠️ 注意
|
||
> - 关闭已关闭的 channel → panic
|
||
> - 向已关闭的 channel 发送数据 → panic
|
||
> - 从已关闭的 channel 读取数据 → 返回零值和 `false`(直到 buf 排空)
|
||
> - 确保发送方全部完成后才关闭 channel,否则 panic
|
||
|
||
### 六、性能建议
|
||
|
||
1. **预知容量时指定 buffer size**:避免运行时动态分配
|
||
2. **优先使用有缓冲 channel**:减少 goroutine 阻塞概率
|
||
3. **关闭 channel 的责任归属**:通常由发送方负责关闭,接收方不应关闭
|
||
4. **无缓冲 channel 适合同步信号**:`make(chan struct{})` 是最轻量的同步方式
|
||
|
||
## 小结
|
||
|
||
- Channel 核心是 `(buf, sendx, recvx)` 环形队列 + `sendq/recvq` 等待链表
|
||
- 无缓冲 channel 的 send/recv 是 G→G 直接传递,不经过 buf
|
||
- 关闭 channel 会批量唤醒所有等待者
|
||
- 理解 channel 底层能有效避免死锁和误用
|
||
|
||
## 关联笔记
|
||
|
||
- [[hzh/GolangStar/Go语言进阶/Channel]] — Channel 的基础用法
|
||
- [[hzh/GolangStar/Go语言进阶/Select]] — Select 多路复用
|
||
- [[hzh/GolangStar/Go面试题库/Channel面试题]] — Channel 相关高频面试题
|