networkpoller 通过将阻塞的 goroutine 挂起(gopark)并注册其文件描述符到 epoll/kqueue/iocp,使 m 立即脱离执行其他任务;待内核通知就绪后,netpoller 唤醒该 g 并将其放回 p 的运行队列,实现 g 阻塞而 m 不阻塞的高效调度。

networkpoller 是如何接管阻塞的 Goroutine 的
当 Goroutine 执行 net.Conn.Read 或 net.Conn.Write 等网络 I/O 操作时,Go 不会直接让 M 进入系统调用阻塞,而是先尝试通过 runtime.netpoll 注册事件。若底层 socket 处于非阻塞模式(Go 默认如此),且当前无数据可读/写,gopark 会被调用,G 进入等待状态,并由 networkpoller 接管。
关键点在于:G 并不和 M 绑定等待,M 会立即脱离、去执行其他 G;而 G 的等待上下文(如 fd、事件类型、回调函数)被注册进 epoll(Linux)、kqueue(macOS)或 iocp(Windows)中。一旦内核通知就绪,netpoll 就唤醒对应 G,将其放回所属 P 的本地队列(runq)或 runnext。
-
networkpoller是一个独立于 M 的轮询线程(通常由 sysmon 或专用 M 启动),它不执行用户代码,只做事件分发 - 每个 P 在初始化时都会关联一个
pollCache,用于复用pollDesc结构体,避免频繁分配 - 如果 G 被 park 后长时间未就绪,
sysmon可能触发超时检查,调用netpolldeadline清理过期等待
为什么 M 不会因网络 I/O 阻塞而被挂起
同步系统调用(如 read(2))会让整个 M 进入内核态休眠,导致 P 无法继续调度其他 G;但 Go 的网络栈绕过了这点——所有标准库网络操作都走 runtime.pollDesc.waitRead / .waitWrite,最终调用 netpollblock,它只 park G,不阻塞 M。
此时 M 会立刻执行调度循环:scheduler() → findrunnable() → 从本地队列、全局队列或其它 P 偷取 G。这种“G 阻塞、M 继续跑”的分离,正是 Go 实现高并发的关键设计。
- 阻塞前,G 的栈和寄存器状态已保存在
g.sched中,M 不需要保留现场 - M 与 P 解绑仅发生在系统调用阻塞(如
open(2)、sleep(3))时,网络 I/O 不触发 handoff - 若 P 的本地队列为空且全局队列也空,M 会进入休眠(
stopm),但不会因单个 G 的网络等待而休眠
异步抢占如何影响 networkpoller 的唤醒路径
Go 1.14+ 的异步抢占(sigPreempt)可能在 G 刚被 netpoll 唤醒、但尚未重新入队的瞬间发生。此时 G 处于“从 park 状态恢复但还未进入可运行队列”的中间态,若被抢占,会先完成唤醒流程再响应信号——这是安全的,因为 goparkunlock 和 goready 是原子配合的。
真正容易出问题的是自定义 poller 场景:比如用 syscall.Syscall 直接调用 epoll_wait,又没正确处理 EINTR,可能导致 G 永远无法被 networkpoller 管理,进而卡死调度器。
- 禁用异步抢占(
GODEBUG=asyncpreemptoff=1)不会影响 networkpoller 正常工作,但会使长循环 G 无法被抢占,间接拖慢唤醒响应 -
runtime_pollWait函数内部有抢占检查点,确保即使在轮询等待中也能响应 GC 或调度指令 - 若 G 在
netpoll唤醒后立即进入密集计算(如无函数调用的 for 循环),可能在下一次调度前耗尽时间片,但不会阻塞 M 或其它 G
调试时如何确认 G 是否被 networkpoller 正确管理
最直接的方式是开启调度 trace:GODEBUG=schedtrace=1000,观察输出中是否有 NETWAIT 状态的 G;或者用 go tool trace 查看 “Network blocking” 事件流。更底层可检查 g.waitreason 字段是否为 waitReasonNetPoller。
注意:pprof goroutine 默认只显示 runnable/blocked 状态,无法区分是系统调用阻塞还是 netpoll 阻塞;需用 runtime.Stack(1) 打印完整栈,查找 netpollblock、runtime_pollWait 等调用帧。
- 在
gdb中可断点runtime.netpoll或internal/poll.runtime_pollWait观察参数fd和mode - 若看到大量 G 长时间处于
waiting且g.waitreason == waitReasonNetPoller,但实际网络无压力,可能是 fd 泄漏或Close未调用 - Linux 下可用
lsof -p $PID | grep IPv4核对打开的 socket 数量,与runtime.NumGoroutine()趋势比对
runnext,以及当前是否存在 STW 或 GC mark 阶段干扰。











