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 指向的连续内存中,sendxrecvx 两个下标绕圈移动:

初始: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 结构,挂到 sendqrecvq 上,并通过 gopark 让出 CPU。

  • recvq:等待接收的 G。当有人来 send 时,直接把数据复制给这个等待的 G,并唤醒它。
  • sendq:等待发送的 G。当有人来 recv 时,直接从等待的 G 复制数据。

无缓冲 channel 的精髓:发送方和接收方必须同时到位。发送方会把数据直接拷贝到接收方的变量里,不需要经过 buf

四、关闭 channel 的语义

关闭 channel 会:

  1. 设置 closed = 1
  2. 唤醒 recvq 中所有等待的 G,让它们收到零值并返回 ok=false
  3. 唤醒 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 关闭的标准方式。

九、逐条解读

  1. make(chan int, 3) 创建带缓冲的 channel,hchan.dataqsiz = 3
  2. 前 3 次发送直接写 buf,不会触发 sendq 或阻塞。
  3. 第 4 次发送时 qcount == dataqsiz,发送 G 被 park 进 sendq
  4. 接收 G 取出 buf[recvx],唤醒 sendq 中的发送 G,后者把 4 写入 buf
  5. 无缓冲 channel 没有 buf,发送方直接把数据拷贝到接收方栈上的变量。
  6. close(ch) 后,recvq 中的等待者收到零值,sendq 中的等待者 panic。
  7. range ch 在关闭后自动退出,不需要额外判断。

十、小结

概念一句话总结
hchanchannel 的底层结构体,包含 buf、锁、sendq/recvq
环形队列缓冲 channel 的 O(1) 入队出队实现
等待队列发送/接收阻塞时,G 被包装成 sudog 挂起
无缓冲 channel发送方直接把数据拷贝给接收方,实现同步握手
关闭 channel通知接收者无更多数据,继续 send 会 panic
select多 channel 加锁后随机遍历,保证公平性
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值