← 返回首页
go的channel底层 有无阻塞
Channel 的底层实现绝对包含阻塞机制。虽然 Channel 的底层数据结构(runtime.hchan)本身是一个包含互斥锁(mutex)和环形缓冲区的结构体,但真正让 Goroutine 发生阻塞和唤醒的,是底层维护的两个双向等待队列:recvq(接收等待队列)和 sendq(发送等待队列)。
根据 Channel 的容量状态和当前操作,底层的阻塞行为分为以下三种核心场景:
1. 无缓冲 Channel(容量为 0):严格的同步阻塞
无缓冲 Channel 的底层没有环形缓冲区,它本质上是一个“数据交接站”。
- 发送方阻塞:当执行
ch <- val时,如果recvq中没有正在等待接收的 Goroutine,当前发送方 G 会被挂起,其 G 对象被封装成sudog节点放入sendq队列中,进入阻塞状态。 - 接收方阻塞:当执行
<- ch时,如果sendq中没有正在等待发送的 Goroutine,当前接收方 G 同样会被挂起,放入recvq队列中阻塞。 - 唤醒机制:只有当发送方和接收方同时到达时,Go Runtime 才会直接将数据从发送方拷贝到接收方,并将双方同时唤醒。
2. 有缓冲 Channel:基于缓冲区状态的异步与阻塞
有缓冲 Channel 底层维护了一个环形数组(buf)。它的阻塞行为完全取决于缓冲区的“满”与“空”:
- 发送方:只要缓冲区未满,发送方将数据写入
buf后直接返回,不会阻塞。只有当缓冲区写满,且recvq中没有等待接收的 G 时,发送方才会被阻塞并放入sendq。 - 接收方:只要缓冲区非空,接收方直接从
buf读取数据,不会阻塞。只有当缓冲区为空,且sendq中没有等待发送的 G 时,接收方才会被阻塞并放入recvq。
3. 阻塞的本质:Goroutine 状态变更
当 Goroutine 因为操作 Channel 被阻塞时,底层 Runtime 会执行以下操作:
- 获取 Channel 内部的互斥锁(
hchan.mutex)。 - 将当前 Goroutine 的状态从
_Grunning(运行中)修改为_Gwaiting(等待中)。 - 将 G 从 P 的本地运行队列中剥离,放入 Channel 的
sendq或recvq中。 - 主动调用调度器(
schedule())去寻找下一个可执行的 G 来接管当前的物理线程 M。
总结:Channel 底层的阻塞并不是像操作系统线程那样陷入内核态的“死等”,而是 Go Runtime 在用户态实现的协程级主动让出(Yield)。当条件不满足时,G 会被挂起在 Channel 的等待队列中,而底层的物理线程 M 则会立刻去执行其他任务,从而实现了高并发下的高效调度。