← 返回首页

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 会执行以下操作:

  1. 获取 Channel 内部的互斥锁(hchan.mutex)。
  2. 将当前 Goroutine 的状态从 _Grunning(运行中)修改为 _Gwaiting(等待中)。
  3. 将 G 从 P 的本地运行队列中剥离,放入 Channel 的 sendq 或 recvq 中。
  4. 主动调用调度器(schedule())去寻找下一个可执行的 G 来接管当前的物理线程 M。

总结:Channel 底层的阻塞并不是像操作系统线程那样陷入内核态的“死等”,而是 Go Runtime 在用户态实现的协程级主动让出(Yield)。当条件不满足时,G 会被挂起在 Channel 的等待队列中,而底层的物理线程 M 则会立刻去执行其他任务,从而实现了高并发下的高效调度。