纤程多路复用
OxPHP 利用 PHP Fiber(纤程)在单个工作进程线程上并发处理多个 HTTP 请求。当某个请求调用 oxphp_sleep() 或 oxphp_async_await()(在启用异步池时)时,它会挂起,工作进程线程随即接手下一个请求。一个工作进程无需额外线程即可管理数百个进行中的请求。
工作原理
调度器在一个线程上运行许多请求,为每个请求分配各自的纤程,并在任一纤程挂起时在它们之间切换。
- 一个请求到达,调度器将其分配给一个纤程:这是一个轻量级的执行上下文,拥有自己的栈和 PHP 状态。
- 纤程运行
oxphp_worker()处理器。如果处理器在没有挂起的情况下完成,响应就会被发送,纤程随即被回收,相比单请求的工作进程零开销。 - 如果处理器调用了挂起函数(
oxphp_sleep()、oxphp_usleep()、oxphp_async_await()),纤程会让出控制权回到调度器。 - 调度器接手新到达的请求(创建新的纤程),并恢复那些等待条件已满足的挂起纤程(定时器到期、异步结果就绪)。
- 每个纤程的 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。不会让出控制权 |
PHP 内置的 sleep() 和 usleep() 会阻塞整个工作进程线程。请始终使用 oxphp_sleep() 和 oxphp_usleep() 以获得协作式行为。
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 调用:
<?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 与其他请求交错执行:
<?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
// 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
$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);数据库连接无法传递给 oxphp_async(),因为对象无法跨线程序列化。请在异步闭包内部创建连接;或者,如果查询足够快、阻塞可以接受,也可以在纤程中直接执行查询。
oxphp_async() 要求 ASYNC_WORKERS > 0。当异步池被禁用(默认情况)时,调用 oxphp_async() 会抛出 OxPHP\Async\AsyncException。
纤程如何被回收
纤程的 C 栈只分配一次,并在多个请求之间复用。当一个纤程处理完一个请求后,它不会被销毁;而是挂起回到调度器,并被加入空闲列表。下一个请求复用现有的 C 栈,从而避免了昂贵的内存分配。
PHP VM 栈(用于函数调用帧)则是每个请求都重新分配,并在处理器返回时释放。
Docker 示例
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
$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 设为一个正值:
ASYNC_WORKERS=8使用 oxphp_async() 时出现 "Failed to dispatch async task"
异步池正在运行,但已达到容量上限。
修复方法: 增大 ASYNC_WORKERS 或 ASYNC_QUEUE_CAPACITY:
ASYNC_WORKERS=8
ASYNC_QUEUE_CAPACITY=512oxphp_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:
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_ENABLED、ENTRY_FILE、PHP_WORKERS、ASYNC_WORKERS