纤程多路复用

OxPHP 利用 PHP Fiber(纤程)在单个工作进程线程上并发处理多个 HTTP 请求。当某个请求调用 oxphp_sleep()oxphp_async_await()(在启用异步池时)时,它会挂起,工作进程线程随即接手下一个请求。一个工作进程无需额外线程即可管理数百个进行中的请求。

工作原理

调度器在一个线程上运行许多请求,为每个请求分配各自的纤程,并在任一纤程挂起时在它们之间切换。

  1. 一个请求到达,调度器将其分配给一个纤程:这是一个轻量级的执行上下文,拥有自己的栈和 PHP 状态。
  2. 纤程运行 oxphp_worker() 处理器。如果处理器在没有挂起的情况下完成,响应就会被发送,纤程随即被回收,相比单请求的工作进程零开销。
  3. 如果处理器调用了挂起函数(oxphp_sleep()oxphp_usleep()oxphp_async_await()),纤程会让出控制权回到调度器。
  4. 调度器接手新到达的请求(创建新的纤程),并恢复那些等待条件已满足的挂起纤程(定时器到期、异步结果就绪)。
  5. 每个纤程的 PHP 状态(超全局变量、响应头、输出缓冲区、VM 栈)在挂起时保存、在恢复时还原。纤程之间完全相互隔离。
graph LR
  W["Worker Thread"]
  W --> A0["Fiber A: handling /api/users"]
  A0 --> A1["oxphp_sleep(0.5)"]
  A1 --> A2["suspended"]
  A2 --> A3["resumed"]
  A3 --> A4["response"]
  W --> B0["Fiber B: handling /api/orders"]
  B0 --> B1["oxphp_async_await($p)"]
  B1 --> B2["suspended"]
  B2 --> B3["resumed"]
  B3 --> B4["response"]
  W --> C0["Fiber C: handling /health"]
  C0 --> C1["response (no suspension, zero overhead)"]

配置

启用工作进程模式后,纤程多路复用会自动激活。没有额外的环境变量需要设置。

变量 默认值 说明
WORKER_MODE_ENABLED false 设为 true 并将 ENTRY_FILE 指向一个 .php 引导文件,即可启用工作进程模式和纤程多路复用
PHP_WORKERS CPU / 2(最小为 1) 工作进程线程数量。每个线程运行各自独立的调度器,最多支持 256 个并发纤程

每个工作进程线程支持的最大并发纤程数为 256。使用 4 个工作进程线程时,OxPHP 最多可同时处理 1,024 个进行中的请求。

挂起点

以下函数会挂起当前纤程,让同一线程上的其他请求得以运行:

函数 发生的行为
oxphp_sleep(float $seconds) 将纤程挂起指定的时长。其他纤程继续运行
oxphp_usleep(int $microseconds) oxphp_sleep() 相同,但精度为微秒(最小为 1 ms)
oxphp_async_await(int $promise_id) 挂起纤程,直到异步任务在后台线程池上完成

以下函数不会挂起纤程:

函数 行为
oxphp_stream_flush() 立即向客户端发送一个数据块并返回。在 SSE 循环中与 oxphp_sleep() 配合使用
oxphp_finish_request() 发送完整响应并继续执行 PHP。不会让出控制权
Note

PHP 内置的 sleep()usleep() 会阻塞整个工作进程线程。请始终使用 oxphp_sleep()oxphp_usleep() 以获得协作式行为。

PHP 示例

基本的并发处理

不挂起的请求以全速运行,没有任何纤程开销:

worker.php
<?php oxphp_worker(function () { $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if ($path === '/health') { echo json_encode(['status' => 'ok']); return; // No suspension — runs at full speed } if ($path === '/slow') { oxphp_sleep(2.0); // Yields for 2 seconds — other requests run echo "Done after 2s delay"; return; } echo "Hello"; });

非阻塞的 API 调用

oxphp_async()oxphp_async_await() 结合,即可在不阻塞工作进程的情况下发起外部 API 调用:

worker.php
<?php oxphp_worker(function () { // Dispatch two API calls to the async thread pool $p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users')); $p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders')); // Await both — the fiber suspends, other requests run on this thread $users = oxphp_async_await($p1); $orders = oxphp_async_await($p2); header('Content-Type: application/json'); echo json_encode(['users' => json_decode($users), 'orders' => json_decode($orders)]); });

配合协作式睡眠的 SSE

使用 oxphp_stream_flush()oxphp_sleep(),让 Server-Sent Events 与其他请求交错执行:

worker.php
<?php oxphp_worker(function () { header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); for ($i = 0; $i < 30; $i++) { echo "data: " . json_encode(['count' => $i, 'time' => time()]) . "\n\n"; oxphp_stream_flush(); // Send chunk now (does not suspend) oxphp_sleep(1.0); // Yield for 1 second (other requests run) } });

阻塞式 I/O

纤程多路复用是协作式的,而非抢占式的。调用阻塞函数的纤程会冻结整个工作进程线程:该线程上的其他纤程都无法继续推进。

会阻塞工作进程的函数

  • file_get_contents()fopen()fread()
  • curl_exec()curl_multi_exec()
  • PDO 查询、mysqli_query()
  • PHP 的 sleep()usleep()(请改用 oxphp_sleep()
  • DNS 解析(gethostbyname()
  • 任何同步的网络或磁盘 I/O

如何避免阻塞

将阻塞操作包裹在 oxphp_async() 中,使其在异步线程池上运行:

php
<?php // WRONG — blocks the entire worker thread $html = file_get_contents('https://example.com'); // CORRECT — runs on async pool, fiber yields $promise = oxphp_async(fn() => file_get_contents('https://example.com')); $html = oxphp_async_await($promise);

对于数据库查询:

php
<?php $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); // WRONG — blocks the worker $users = $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); // CORRECT — query runs on async thread, fiber yields $promise = oxphp_async(function () { $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); return $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); }); $users = oxphp_async_await($promise);
Note

数据库连接无法传递给 oxphp_async(),因为对象无法跨线程序列化。请在异步闭包内部创建连接;或者,如果查询足够快、阻塞可以接受,也可以在纤程中直接执行查询。

Warning

oxphp_async() 要求 ASYNC_WORKERS > 0。当异步池被禁用(默认情况)时,调用 oxphp_async() 会抛出 OxPHP\Async\AsyncException

纤程如何被回收

纤程的 C 栈只分配一次,并在多个请求之间复用。当一个纤程处理完一个请求后,它不会被销毁;而是挂起回到调度器,并被加入空闲列表。下一个请求复用现有的 C 栈,从而避免了昂贵的内存分配。

PHP VM 栈(用于函数调用帧)则是每个请求都重新分配,并在处理器返回时释放。

Docker 示例

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "80:80" environment: - DOCUMENT_ROOT=/var/www/html/public - WORKER_MODE_ENABLED=true - ENTRY_FILE=worker.php - PHP_WORKERS=4 - ASYNC_WORKERS=8

采用这一配置后,4 个工作进程线程中的每一个都能处理多达 256 个并发纤程,而阻塞式 I/O 则被卸载到 8 个异步工作进程线程上。

故障排查

当某个请求执行繁重的 I/O 时,请求变慢

某个纤程在没有使用 oxphp_async() 的情况下调用了阻塞函数(数据库查询、HTTP 请求、文件读取)。这会阻塞整个工作进程线程。

修复方法: 将阻塞调用包裹在 oxphp_async() 中:

php
<?php $promise = oxphp_async(fn() => file_get_contents($url)); $result = oxphp_async_await($promise);
"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."

异步池尚未配置。当 ASYNC_WORKERS=0(默认情况)时,所有异步函数都会抛出 OxPHP\Async\AsyncException

修复方法:ASYNC_WORKERS 设为一个正值:

bash
ASYNC_WORKERS=8
使用 oxphp_async() 时出现 "Failed to dispatch async task"

异步池正在运行,但已达到容量上限。

修复方法: 增大 ASYNC_WORKERSASYNC_QUEUE_CAPACITY

bash
ASYNC_WORKERS=8 ASYNC_QUEUE_CAPACITY=512
oxphp_sleep() 没有让出控制权给其他请求

纤程多路复用仅在工作进程模式下有效。在传统模式下,oxphp_sleep() 会退化为阻塞式的 usleep()

修复方法: 通过设置 WORKER_MODE_ENABLED=true 来启用工作进程模式。

大量并发请求时内存占用过高

每个纤程会使用一个 C 栈(默认 8 MiB,由 PHP 的 fiber.stack_size ini 设置配置),外加每个请求一个 PHP VM 栈。在 256 个并发纤程的情况下,最坏情况下每个工作进程线程的 C 栈内存为 2 GiB。

修复方法: 如果你的应用不使用深度递归,可以在 php.ini 中减小 fiber.stack_size

php.ini
fiber.stack_size = 512K

限制

  • 仅限工作进程模式 — 纤程多路复用在传统模式下不可用
  • 每个工作进程 256 个纤程 — 这是硬性上限,运行时不可配置
  • 仅协作式 — CPU 密集型代码(紧凑循环、繁重计算)会饿死其他纤程。没有抢占机制
  • 阻塞式 I/O 会阻塞线程 — 所有阻塞调用都必须包裹在 oxphp_async() 中才能实现真正的并发
  • PHP 原生的 sleep()/usleep() 不感知纤程 — 请使用 oxphp_sleep()/oxphp_usleep()
  • oxphp_async_await_race()oxphp_async_await_any() 不会让出控制权 — 它们目前即使在纤程内部也会阻塞。oxphp_async_await_all() 确实会在等待时挂起纤程,因此它对纤程友好;对于 race/any,如果你需要让线程保持协作式,请改用顺序的 oxphp_async_await() 调用

参见

  • 工作进程模式 — 持久化的 PHP 进程与 oxphp_worker() API
  • 异步 Promise — 用于卸载阻塞式 I/O 的后台线程池
  • SSE — 实时流式传输,结合基于纤程的协作式睡眠
  • PHP 函数oxphp_sleep()oxphp_usleep() 及其他感知纤程的函数
  • 配置参考WORKER_MODE_ENABLEDENTRY_FILEPHP_WORKERSASYNC_WORKERS