是的,G 进入 Go 运行时维护的 channel 内部队列(sendq/recvq),而非操作系统级等待队列;其通过 sudog 结构体记录 goroutine、待发送/接收元素及对应 channel。

channel 阻塞时,G 真的进等待队列了吗?
是的,但不是操作系统级的等待队列,而是 Go 运行时维护的 channel 内部队列。当 g 执行 ch 或 <code> 且无法立即完成(比如无缓冲 channel 无人接收、有缓冲但已满/空),该 <code>g 的状态会从 _Grunning 变为 _Gwaiting,并被链入 channel 的 recvq(读等待队列)或 sendq(写等待队列)。这个过程不涉及系统调用,也不释放 M,P 会立刻去调度下一个 g。
关键点在于:这些队列是纯内存结构,由 runtime 在用户态管理;g 不会陷入内核等待,所以不会触发线程切换开销。
唤醒一个被 channel 阻塞的 G,谁来触发?
唤醒动作由执行对应操作的另一个 g 完成。比如:g1 在 recvq 中等待,g2 向该 channel 发送数据,runtime 会在完成发送后立即从 recvq 头部取出 g1,将其状态设为 _Grunnable,并放入某个 P 的本地运行队列(runq)或全局队列(GRQ)。
- 优先放回原
P的本地队列(如果该P仍可用) - 若原
P已不可用(如正在执行阻塞系统调用),则放入全局队列,由其他P后续窃取 - 唤醒过程不经过
sysmon或网络轮询器,完全同步、无延迟
channel 等待队列和 P 的本地队列有什么区别?
两者完全隔离,目的和生命周期不同:
-
P.runq:存放所有就绪可运行的g,由M轮询调度;无锁访问,容量默认 256 -
channel.recvq/sendq:仅存放因 channel 操作而阻塞的g;需加锁(因为多个g可能并发进出),结构是sudog链表,不参与调度循环 - 一个
g不可能同时在两个队列里;它要么在P.runq(就绪),要么在某个 channel 的recvq/sendq(等待),要么在GRQ(全局兜底)
为什么有时看到 channel 等待的 G 长时间没被唤醒?
常见原因不是调度器问题,而是逻辑死锁或资源错配:
- 向一个无缓冲 channel 发送,但没有 goroutine 在另一端接收 ——
g永远卡在sendq - 向带缓冲 channel 发送,但缓冲已满,且无其他
g接收或消费 —— 同样卡在sendq - 使用
select时漏写了default,又没其他 case 就绪,导致阻塞在 channel 操作上 - channel 被关闭后,读端仍尝试接收(虽不会阻塞,但可能误判为“没唤醒”);写端再写会 panic:
send on closed channel
真正难排查的是:channel 被多个 P 上的 goroutine 共享,而唤醒路径依赖精确的配对顺序。这种场景下,g 的等待/唤醒行为看似随机,其实是确定性的,只是你没画出完整的通信图谱。


















