PHP 8.6 的 Io\Poll:用 epoll/kqueue 做轮询,而不只是 stream_select
PHP 一直靠 stream_select() 监听流,但它有局限,也无法暴露现代操作系统提供的更丰富的轮询能力。PHP 8.6 通过 Polling API RFC 引入了 Io\Poll。
最小可运行示例
原文示例只给出了轮询骨架,把 socket 的来源补上就是一个可以直接跑的 TCP echo 服务:
use Io\Poll\{Context, Event};
$server = stream_socket_server('tcp://127.0.0.1:9000', $errno, $errstr);
stream_set_blocking($server, false);
$poll = new Context();
$poll->add(
$server,
[Event::Readable],
);
while (true) {
foreach ($poll->wait() as $event) {
$stream = $event->getHandle()->getStream();
// 监听 socket 上有可读事件:接受新连接并注册进同一个 Context
if ($stream === $server) {
$client = stream_socket_accept($server, 0);
if ($client !== false) {
stream_set_blocking($client, false);
$poll->add($client, [Event::Readable]);
}
continue;
}
$data = fread($stream, 8192);
// Process the incoming data...
}
}
核心只有三步:new Context() 建轮询上下文,add() 把流和关心的事件注册成 watcher,wait() 阻塞并返回就绪事件;从事件里用 $event->getHandle()->getStream() 取回原来的流。
抽象与后端
API 围绕这几样东西设计:
- 轮询上下文(
Context) - watcher(
add()的返回值,代表一个被监视的句柄) - 可读 / 可写事件(
Event::Readable等) - 平台相关的后端
- 回调式事件处理
- 在平台支持时使用边缘触发轮询
后端按平台自动选择:Linux 上是 epoll,BSD/macOS 上是 kqueue,其余情况回退到 poll。这层差异对使用者是透明的,也是它比 stream_select() 更适合事件循环、网络服务器、代理和其它长时间运行的 I/O 负载的原因。
与 stream_select 的差别和限制
stream_select() 每次调用都要重新组装 fd 数组并做线性扫描,且受 fd 集合大小上限约束;Io\Poll 把注册(add())和等待(wait())分开,由内核侧的后端维护监视集合,规模上来后不用每轮重建。
但它不是 stream_select() 的万能替代:
- 后端由平台决定,边缘触发也只在部分平台可用,跨平台行为并不完全一致。
- 短小的 CLI 脚本、连接数很少的场景,
stream_select()够用,也省去理解 watcher 生命周期的成本。 Io\Poll是新 API,不是既有代码的即插即换。
注意事项
边缘触发要读到空。 在边缘触发模式下,事件只在状态变化时通知一次。上面示例里 fread($stream, 8192) 只读一次,如果缓冲区里还有数据就不会再有新事件,必须循环读到 fread() 返回空字符串或 false(EAGAIN)为止。
流必须是非阻塞的。 注册前先 stream_set_blocking($stream, false),否则一次读操作会把整个事件循环卡住。
watcher 的注销要自己管。 连接关闭时把对应 watcher 从 Context 中移除,否则会持续收到已失效句柄的事件。示例为了简短省略了这部分。
同版本另外两项改动
方向感知的属性解析:PHP 把非对称属性的写可见性检查前移到运行时缓存填充阶段,一旦确定某个调用点有写权限,后续写入直接复用缓存结果。语法不变,private(set) / protected(set) 属性的写入变便宜了。
流错误处理:流上下文可以声明错误处理方式。
$context = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Exception,
],
]);
$content = file_get_contents('/path/to/missing-file', context: $context);
三种模式:StreamErrorMode::Error 保持常规的 PHP 警告和通知;StreamErrorMode::Exception 对终止性错误抛异常;StreamErrorMode::Silent 抑制错误输出。异常模式下可以抛出 StreamException,RFC 同时加入了结构化错误存储、错误码和自定义错误回调支持。该行为必须通过显式上下文开启,不能经由默认流上下文全局生效,以保证兼容性。