定义原生 Polling API 的 PHP RFC 已获批准并合并到语言的 master 分支中,这为开发者提供了一种快速、操作系统级别的文件描述符轮询方式。结合最近添加的 Fibers,PHP 现在拥有了构建异步代码的高效内置基础——在此之前,这一空白长期迫使各种库不得不自行拼凑各种变通方案。
Fibers 与事件循环的分离
Fibers 是一种底层的控制流原语。它们允许函数在选定的点挂起执行,并在稍后从完全相同的位置恢复。至关重要的一点是,fiber 对 socket、定时器或任何其他 I/O 源一无所知;它只是根据需求暂停和重启。
事件循环是决定何时恢复已挂起 fiber 的调度器。在典型的异步栈中,循环会监听一组文件描述符,等待它们变为可读或可写,然后唤醒相应的 fiber。
PHP 此前虽有 Fibers,但缺乏一种原生机制来询问操作系统哪些描述符已就绪。其结果是不得不依赖 stream_select() 或外部扩展,导致每个异步库最终都不得不为 Linux 的 epoll、BSD 的 kqueue、Windows 的 IOCP 等编写自己的后端。
新的 Polling API 填补了这一缺失的环节。它为操作系统的轮询设施(Linux 上的 epoll、BSD/macOS 上的 kqueue 等)提供了一个轻量级的封装。该 API 并不取代 Fibers;它只是为事件循环提供了所需的性能和可扩展性。
为什么这一变化对 ReactPHP、Amp 及其他库至关重要
ReactPHP 和 Amp v3 在操作系统的轮询机制之上构建了自己的抽象层。这些层包含多条代码路径,每条路径都针对特定平台进行了优化,并且必须与内核的变化保持同步。有了原生的 Polling API,这些库可以舍弃大部分此类底层实现,转而依赖单一的核心 API 调用。
- 维护 – 更少的平台特定分支意味着更少的 Bug 以及更小的安全审查范围。
- 性能 – 原生调用直接与 epoll/kqueue 通信。
- 可移植性 – 在“原生”PHP 上运行的代码现在可以在所有支持的操作系统上获得相同的基准性能,而无需可选扩展。
相比之下,Swoole 仍然是一个完整的运行时替换方案,自带事件循环和协程系统。Polling API 不会影响 Swoole 的权衡;需要极低延迟或自定义内存管理的开发者仍会将其作为一个独立的选项。
快速基准测试说明了一切
一个极简的调度器通过原始 socket 获取了几个 URL,并使用单个 Io\Poll\Context 来管理多个 fiber。实验得出了两个观察结果:
- 速度 – 增加更多的并发请求不会增加总耗时;请求批次的完成时间与最慢的单个请求相当。换句话说,并发开销实际上为零。
- CPU 开销 – 使用
stream_select()时,随着流数量的增加,CPU 使用率会明显上升,因为该函数在每次调用时必须遍历每个描述符。而 Polling API 的开销在从 3 个流增加到 30 个流时保持平稳,这得益于内核能够在单个系统调用中监控大量描述符的能力。
开发者现在应该怎么做
- 采用 Amp v3,如果你更倾向于与新 API 直接对齐的 fiber 原生风格。它的公共接口已经映射到了底层的 poller,因此你无需重写代码即可获益。
- 坚持使用 ReactPHP,如果你喜欢对循环生命周期进行显式控制。
- 避免在生产负载中使用自定义调度器。不要为生产环境编写自己的调度器。
