编程 PHP 8.6 的 Io\Poll:用 epoll/kqueue 做事件循环,和 stream_select 的差别与三个坑

2026-11-10 21:02:48

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 同时加入了结构化错误存储、错误码和自定义错误回调支持。该行为必须通过显式上下文开启,不能经由默认流上下文全局生效,以保证兼容性。

来源:What's new in PHP 8.6 — Amit Merchant

复制全文 生成海报 PHP 事件循环 Socket epoll

推荐文章

程序员茄子在线接单