核心定位
这是一个纯引擎内部机制的 RFC,不添加任何 PHP 可见的类、函数、常量或语法。它的目的是在 PHP 引擎层面建立一个可插拔的协程调度器接口(ABI),让扩展可以提供调度器,从而激活 PHP 的并发执行模式。
注意:这里说的"并发"是单线程协作式的,不是多线程并行。所有执行流都在一个 OS 线程内,通过协程切换实现交错执行。
RFC:https://wiki.php.net/rfc/async_scheduler_abi
背景与动机
- PHP 8.1 引入了 Fiber(纤程)作为底层原语,但没有调度器——何时运行什么、按什么顺序,完全交给用户态处理。
- 结果是 ReactPHP、Revolt、AMPHP 等框架各自维护独立的事件循环和协程抽象,互不兼容。
- 这个 RFC 填补了这个空白:在引擎层面定义一个调度器插槽,让扩展可以注册一个调度器来驱动协程。
提案核心:调度器 ABI
引擎与调度器之间通过六个通知点(notifications)交互。引擎自己不调度,所有调度决策都由调度器通过通知的返回值来做出。
六个引擎通知点
| | |
|---|
| Launch | | |
| Suspend | | 管理就绪队列,决定接下来运行哪个协程;主协程结束时需返回一个新的主协程给后续代码(如析构函数、shutdown 函数) |
| Enqueue | | 将协程加入就绪队列;可附带一个错误(用于取消、超时、I/O 失败) |
| Foreign Fiber | 第三方代码创建的 Fiber 启动时(调度器已激活) | 决定是"收养"该 fiber 到自己的调度中,还是让它保持为普通低层 fiber |
| Defer | | |
| Shutdown | 并发需要提前终止时(如某个协程调用了 exit()) | |
关键机制
- 对称切换(Symmetric Switch):不同于 Fiber 的"不对称"(只能 yield 给恢复者),这里 A 可以直接切换到 B,减少切换开销。
- 协程生命周期:
created → queued → running → suspended → finished,可循环。 - 主协程(Main):顶层脚本本身作为一个协程运行,结束时会被替换而非回收,以便后续 shutdown 代码继续运行。
协程上下文存储
引擎为每个协程维护两个存储,解决全局状态在并发下的安全问题:
- Internal Store(内部存储):供引擎和 C 扩展使用,键是数字 ID,值是原始 C 数据,对 PHP 不可见。用于存放如
ob_start() 的处理器栈、gethostbyname() 的结果缓冲区等。 - Userland Store(用户态存储):存普通 PHP 值(字符串/对象作键),是否暴露给 PHP 由调度器扩展决定。
存储机制已实现,但将 ob_start() 等核心子系统迁移到 per-coroutine 状态是后续工作。
与现有生态的关系
| |
|---|
| 无调度器注册时 | |
| Fiber 库(ReactPHP/Revolt/AMPHP) | 现有代码无需改动;调度器可选择"收养"它们的 fiber,让它们运行在同一个调度上 |
| 调试工具(Xdebug 等) | 协程执行上下文基于现有 Fiber 机制,step debugging 和堆栈跟踪继续工作 |
| Opcache / JIT | |
| SAPI(CLI/FPM 等) | |
向后不兼容的变更(仅当调度器激活时)
Fiber::suspend() 在析构函数中抛出错误
- 调度器激活时,GC 析构阶段在一个专用协程中运行,该协程不是 fiber,因此从中调用
Fiber::suspend() 会抛出 FiberError。 - 这是有意为之的权衡:让析构阶段可预测,避免任意阻塞某个执行流。
gc_collect_cycles() 可能因新原因返回 0 专用协程被取消,或无法创建专用协程时,返回 0。
进程 fork 被限制 调度器激活时,pcntl_fork() 默认抛出错误,除非调度器注册了专门的 fork 处理程序。
明确不在这个 RFC 中的内容
这个 RFC 刻意保持最小化,只定义引擎与调度器的接缝。以下属于后续工作:
- 非阻塞 I/O:socket、文件、DNS、定时器的异步层
- 用户可见的 API:如
spawn()、await()、channels、futures、Async\ 命名空间等 - 抢占式调度:所有切换都是显式的;抢占需要基于引擎现有中断机制的额外工作
- Goroutine 式 M:N 调度 / 并行执行:PHP 的运行时架构(每线程独立对象存储、非原子引用计数)决定了这不可行