FEATURED · 精选文章

Golang Channel 底层:hchan、环形队列与等待队列的精准握手

发布时间 / 2026/9/10 23:04:50
来源 / 创域科博编辑部
栏目 / 资讯中心
Golang Channel 底层:hchan、环形队列与等待队列的精准握手 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两个下标绕圈移动初始sendx0, recvx0, qcount0 send 1buf[0]1, sendx1, qcount1 send 2buf[1]2, sendx2, qcount2 recv 1vbuf[0], recvx1, qcount1 send 3buf[2]3, sendx3(模回0), qcount2因为是环形不需要移动数据只需要移动下标入队出队都是 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让它们收到零值并返回okfalse。唤醒sendq中所有等待的 G触发 panic向已关闭 channel 发送。所以关闭一个已经关闭的 channel、或者关闭后 send都会 panic。五、select 的随机性从哪里来select在 runtime 层面会先把所有 case 涉及的 channel 都加锁然后随机打乱 case 顺序遍历。如果有多个 case 就绪选中顺序是伪随机的。这也是 Go select 设计中的一个重要保证不会总是优先某个 case。六、代码实践用缓冲 channel 验证环形队列行为下面这个程序创建容量为 3 的 channel手动发送接收观察顺序和阻塞。packagemainimport(fmttime)funcmain(){ch:make(chanint,3)// 发送 3 个不阻塞fori:1;i3;i{ch-i fmt.Printf(sent %d\n,i)}// 第 4 个发送会阻塞除非有 goroutine 接收gofunc(){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-4fmt.Println(4th send succeeded after receiver took one)// 清空剩余forv:rangech{fmt.Printf(drain %d\n,v)iflen(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为 nildataqsiz为 0。发送和接收必须配对发生。下面验证无缓冲 channel 的同步性。packagemainimport(fmtsynctime)funcmain(){ch:make(chanint)varwg sync.WaitGroup wg.Add(2)gofunc(){deferwg.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))}()gofunc(){deferwg.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 遍历packagemainimportfmtfuncmain(){ch:make(chanint,3)ch-1ch-2close(ch)forv:rangech{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: v0 okfalserange会在 channel 关闭且没有数据后自然退出okfalse是判断 channel 关闭的标准方式。九、逐条解读make(chan int, 3)创建带缓冲的 channelhchan.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在关闭后自动退出不需要额外判断。十、小结概念一句话总结hchanchannel 的底层结构体包含 buf、锁、sendq/recvq环形队列缓冲 channel 的 O(1) 入队出队实现等待队列发送/接收阻塞时G 被包装成 sudog 挂起无缓冲 channel发送方直接把数据拷贝给接收方实现同步握手关闭 channel通知接收者无更多数据继续 send 会 panicselect多 channel 加锁后随机遍历保证公平性
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻