tech

Go netpoller 原理速记

从同步调用到异步轮询,拆解 Go 语言 netpoller 如何将 epoll 封装成同步非阻塞模型,以及 Goroutine 在 EAGAIN 时如何被挂起和唤醒。

Go 语言的 netpoller 是实现高并发网络编程的核心组件。它基于操作系统提供的 I/O 多路复用机制(Linux 下是 epoll),将复杂的异步 I/O 操作封装成开发者熟悉的同步非阻塞模型。


从同步调用到异步轮询

1. 初始化与注册

当一个网络服务启动时,比如调用 net.Listen,Go 运行时会初始化一个全局唯一的 epoll 实例。所有监听或连接得到的 socket 文件描述符(fd)都会被设置为非阻塞模式,并注册到这个 epoll 实例上,由 netpoller 统一管理。

2. “同步”读写背后的真相

这是最关键的一步。当你执行 conn.Readconn.Write 时,流程如下:

① 尝试读写

操作会先在非阻塞的 fd 上进行。这一步的关键是“尝试读写”——也就是非阻塞的轮询查询。Go 的 netpoll 函数(封装了 epoll_wait永远不会传入 -1(表示无限阻塞),只会在 0 和一个极短的超时时间(如 0ms10ms)之间切换。

② 遇到 EAGAIN

如果数据还没准备好(例如读操作时 socket 缓冲区为空),系统调用会返回 EAGAIN 错误,表示“现在无法完成操作”。

③ 挂起 Goroutine

此时,netpoller 并不会让操作失败返回,而是将当前发起调用的 Goroutine 挂起(park),使其让出 CPU,不再占用线程 M。调度器会立刻从本地或全局运行队列中取出另一个 G,放到这个 M 上去运行。这个被挂起的 Goroutine 会被记录在与该 fd 关联的等待队列中。

3. 事件唤醒与恢复

当 epoll 实例检测到某个 fd 变为可读或可写时,它会通知 netpoller。netpoller 根据就绪的 fd,找到等待队列中的 Goroutine,将其状态重新设置为可运行(runnable),等待调度器调度执行。当这个 Goroutine 再次获得 CPU 时间并执行被中断的 ReadWrite 操作时,数据已经就绪,操作便能成功完成。从开发者的角度看,这就是一次“阻塞”操作的成功返回。


对比:普通阻塞 vs. netpoller

场景 行为
普通阻塞系统调用 G 和 M 一起卡住,M 被占用,P 解绑,P 去找新 M 运行别的 G
网络 IO(netpoller 托管) 只有 G park 休眠,M 释放回 P,不会被阻塞

netpoller 的巧妙之处在于,它让成千上万个 Goroutine 可以高效地共享少量 OS 线程,而开发者依然可以像写同步代码一样使用 Read / Write,无需显式处理异步回调。