Channel 底层:hchan、环形队列与等待队列的精准握手
Channel 是 Go 并发设计的灵魂。表面看它是 chan T,背后却是 runtime.hchan 结构体,配合环形缓冲区、两把等待队列和一把锁,实现了 CSP 模型中“通过通信共享内存”的语义。
一、hchan 结构体
Go 源码中 channel 的底层结构简化如下:
type hchan struct {
qcount uint // 当前队列中的元素个数
dataqsiz uint // 环形队列大小(缓冲 channel 的容量)
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素大小
closed uint32 // 是否已关闭
elemtype *_type // 元素类型元数据
sendx uint // 发送索引
recvx uint // 接收索引
recvq waitq // 等待接收的 goroutine 队列
sendq waitq // 等待发送的 goroutine 队列
lock mutex // 保护 hchan 的锁
}
关键洞察:无论缓冲 channel 还是无缓冲 channel,都是同一个 hchan 结构。区别只是 dataqsiz 是否为 0。
二、环形队列
缓冲 channel 的元素存在 buf 指向的连续内存中,sendx 和 recvx 两个下标绕圈移动:
初始:sendx=0, recvx=0, qcount=0
send 1:buf[0]=1, sendx=1, qcount=1
send 2:buf[1]=2, sendx=2, qcount=2
recv 1:v=buf[0], recvx=1, qcount=1
send 3:buf[2]=3, sendx=3(模回0), qcount=2
因为是环形,不需要移动数据,只需要移动下标,入队出队都是 O(1)。
三、等待队列
当 channel 没有空间发送或没有元素接收时,goroutine 不会阻塞 OS 线程,而是被包装成 sudog 结构,挂到 sendq 或 recvq 上,并通过 gopark 让出 CPU。
recvq:等待接收的 G。当有人来 send 时,直接把数据复制给这个等待的 G,并唤醒它。sendq:等待发送的 G。当有人来 recv 时,直接从等待的 G 复制数据。
无缓冲 channel 的精髓:发送方和接收方必须同时到位。发送方会把数据直接拷贝到接收方的变量里,不需要经过 buf。
四、关闭 channel 的语义
关闭 channel 会:
- 设置
closed = 1。 - 唤醒
recvq中所有等待的 G,让它们收到零值并返回ok=false。 - 唤醒
sendq中所有等待的 G,触发 panic(向已关闭 channel 发送)。
所以关闭一个已经关闭的 channel、或者关闭后 send,都会 panic。
五、select 的随机性从哪里来
select 在 runtime 层面会先把所有 case 涉及的 channel 都加锁,然后随机打乱 case 顺序遍历。如果有多个 case 就绪,选中顺序是伪随机的。这也是 Go select 设计中的一个重要保证:不会总是优先某个 case。
六、代码实践:用缓冲 channel 验证环形队列行为
下面这个程序创建容量为 3 的 channel,手动发送接收,观察顺序和阻塞。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
// 发送 3 个,不阻塞
for i := 1; i <= 3; i++ {
ch <- i
fmt.Printf("sent %d\n", i)
}
// 第 4 个发送会阻塞,除非有 goroutine 接收
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("receiver waking up")
v := <-ch
fmt.Printf("received %d\n", v)
}()
fmt.Println("about to block on 4th send")
ch <- 4
fmt.Println("4th send succeeded after receiver took one")
// 清空剩余
for v := range ch {
fmt.Printf("drain %d\n", v)
if len(ch) == 0 {
break
}
}
}
运行结果:
sent 1
sent 2
sent 3
about to block on 4th send
receiver waking up
received 1
4th send succeeded after receiver took one
drain 2
drain 3
drain 4
七、无缓冲 channel 的精准握手
无缓冲 channel 的 buf 为 nil,dataqsiz 为 0。发送和接收必须配对发生。下面验证无缓冲 channel 的同步性。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("sender: preparing data at", time.Now().Format("15:04:05.000"))
ch <- 42 // 会阻塞,直到接收方准备好
fmt.Println("sender: handoff complete at", time.Now().Format("15:04:05.000"))
}()
go func() {
defer wg.Done()
time.Sleep(200 * time.Millisecond)
fmt.Println("receiver: ready at", time.Now().Format("15:04:05.000"))
v := <-ch
fmt.Println("receiver: got", v)
}()
wg.Wait()
}
输出显示:发送方在 ch <- 42 处等待,直到 200ms 后接收方才取出数据。
八、关闭 channel 与 range 遍历
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println("range got", v)
}
v, ok := <-ch
fmt.Printf("after close: v=%d ok=%v\n", v, ok)
}
输出:
range got 1
range got 2
after close: v=0 ok=false
range 会在 channel 关闭且没有数据后自然退出,ok=false 是判断 channel 关闭的标准方式。
九、逐条解读
make(chan int, 3)创建带缓冲的 channel,hchan.dataqsiz = 3。- 前 3 次发送直接写
buf,不会触发sendq或阻塞。 - 第 4 次发送时
qcount == dataqsiz,发送 G 被 park 进sendq。 - 接收 G 取出
buf[recvx],唤醒sendq中的发送 G,后者把 4 写入buf。 - 无缓冲 channel 没有
buf,发送方直接把数据拷贝到接收方栈上的变量。 close(ch)后,recvq中的等待者收到零值,sendq中的等待者 panic。range ch在关闭后自动退出,不需要额外判断。
十、小结
| 概念 | 一句话总结 |
|---|---|
| hchan | channel 的底层结构体,包含 buf、锁、sendq/recvq |
| 环形队列 | 缓冲 channel 的 O(1) 入队出队实现 |
| 等待队列 | 发送/接收阻塞时,G 被包装成 sudog 挂起 |
| 无缓冲 channel | 发送方直接把数据拷贝给接收方,实现同步握手 |
| 关闭 channel | 通知接收者无更多数据,继续 send 会 panic |
| select | 多 channel 加锁后随机遍历,保证公平性 |

4390

被折叠的 条评论
为什么被折叠?



