Go netpoll 本身不引发上下文切换 你看到的goroutine 阻塞在conn.Read(),不是线程(M)被挂起,而是 goroutine 被gopark挂起、让出 M 给其他 G 使用。此时 M 仍在运行它立刻去执行别的 goroutine,不会触发
Go netpoll 本身不引发上下文切换你看到的 goroutine 阻塞在 conn.Read(),不是线程(M)被挂起,而是 goroutine 被 gopark 挂起、让出 M 给其他 G 使用。此时 M 仍在运行——它立刻去执行别的 goroutine,不会触发 OS 级上下文切换。 真正触发上下文切换的,是当你手动创建大量线程(比如用 syscall.Clone)、或开了太多阻塞型系统调用(如未配对的 runtime.Entersyscall),又或者用了 CGO_ENABLED=1 且 C 函数长期阻塞。
手动调 epoll 会强制引入上下文切换一旦绕过 runtime 直接调用 syscall.EpollWait,你就脱离了 netpoll 的调度闭环:goroutine 无法被自动 park/unpark,只能靠自己用 runtime.Entersyscall 进入系统调用态——这会让 M 脱离 P,触发一次完整的 OS 级上下文切换;等事件返回再 runtime.Exitsyscall,又要切回来。
对比 select/poll 为什么更伤上下文切换Linux 下 select 和 poll 每次调用都要把整个 fd 集合从用户态拷贝到内核态,返回时再把就绪集合拷回;epoll 是一次性注册、增量更新,但它们共通的问题是:调用者必须自己维护事件循环线程——这个线程一旦阻塞在 select 或 poll 上,就只能干等,无法同时跑业务逻辑。
真正该盯的指标不是 epoll_wait 耗时别花时间看 strace -e epoll_wait 的平均延迟——那只是内核通知快慢,和你的吞吐无关。netpoll 的性能瓶颈从来不在 epoll 层,而在上层是否让 goroutine 快速进出 IO 等待态。
netpoll 的设计目标就是消灭不必要的上下文切换,但它无法保护你写错的业务逻辑。goroutine 卡住的真正原因,99% 都藏在你自己写的 for 循环、锁、或没设 deadline 的 Read 里。 |
2022-04-28
2022-04-21
2022-05-13
2022-08-17
2024-05-07