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.Read 或 conn.Write 时,流程如下:
① 尝试读写
操作会先在非阻塞的 fd 上进行。这一步的关键是“尝试读写”——也就是非阻塞的轮询查询。Go 的 netpoll 函数(封装了 epoll_wait)永远不会传入 -1(表示无限阻塞),只会在 0 和一个极短的超时时间(如 0ms 或 10ms)之间切换。
② 遇到 EAGAIN
如果数据还没准备好(例如读操作时 socket 缓冲区为空),系统调用会返回 EAGAIN 错误,表示“现在无法完成操作”。
③ 挂起 Goroutine
此时,netpoller 并不会让操作失败返回,而是将当前发起调用的 Goroutine 挂起(park),使其让出 CPU,不再占用线程 M。调度器会立刻从本地或全局运行队列中取出另一个 G,放到这个 M 上去运行。这个被挂起的 Goroutine 会被记录在与该 fd 关联的等待队列中。
3. 事件唤醒与恢复
当 epoll 实例检测到某个 fd 变为可读或可写时,它会通知 netpoller。netpoller 根据就绪的 fd,找到等待队列中的 Goroutine,将其状态重新设置为可运行(runnable),等待调度器调度执行。当这个 Goroutine 再次获得 CPU 时间并执行被中断的 Read 或 Write 操作时,数据已经就绪,操作便能成功完成。从开发者的角度看,这就是一次“阻塞”操作的成功返回。
对比:普通阻塞 vs. netpoller
| 场景 | 行为 |
|---|---|
| 普通阻塞系统调用 | G 和 M 一起卡住,M 被占用,P 解绑,P 去找新 M 运行别的 G |
| 网络 IO(netpoller 托管) | 只有 G park 休眠,M 释放回 P,不会被阻塞 |
netpoller 的巧妙之处在于,它让成千上万个 Goroutine 可以高效地共享少量 OS 线程,而开发者依然可以像写同步代码一样使用 Read / Write,无需显式处理异步回调。
