用 Fiber 封装可暂停任务:启动、挂起与异常恢复
我第一次把 PHP Fiber 放进任务代码时,最麻烦的并不是 suspend() 本身,而是调用方很快散落了大量状态判断:第一次要用 start(),挂起后才能 resume(),异常要从挂起点注入,结束后又只能在正常返回时读取 getReturn()。把这些细节收进一个 PausableTask,业务层会清楚很多。
PHP Fiber 官方手册:https://www.php.net/manual/en/language.fibers.php
本文给出的最小配方是:由包装类持有一个 Fiber,外层只调用 start()、resume()、inject() 和 result();任务内部用 Fiber::suspend() 交出控制权。可恢复异常在挂起点被捕获并转换为 recoverable 信号,调用方再决定重试还是终止。
先分清两组返回值
Fiber 的数据交换是双向的,这一点最容易绕晕。Fiber::suspend($out) 里的 $out 会返回给外层正在执行的 start()、resume() 或 throw();下一次外层调用 resume($in) 时,$in 又会成为上一次 Fiber::suspend() 的返回值。
'waiting']);
// 中文说明:resume() 传入的值会从 suspend() 返回
return 'processed:' . $payload;
});
$signal = $fiber->start(); // 中文说明:得到第一个挂起值
$fiber->resume('order-42'); // 中文说明:任务继续并正常返回
$result = $fiber->getReturn(); // 中文说明:只在正常终止后读取最终返回值
start() 只能用于尚未启动的 Fiber;resume() 和 throw() 要求 Fiber 正处于挂起状态。违反这些状态约束会得到 FiberError。这也是我更愿意提供包装类的原因:不让每个调用点重复记忆同一套前置条件。
最小封装:把状态判断收进 PausableTask
下面的包装类先解决三个问题:保存 Fiber、统一检查可调用状态、记录无法在任务内部恢复的异常。它没有实现事件循环,也没有偷偷启动线程,只是把协作式控制权交换变成稳定接口。
fiber = new Fiber(function () use ($work): mixed {
while (true) {
try {
// 中文说明:先报告可接收任务,再等待外层送入 payload
$payload = Fiber::suspend([
'state' => 'waiting',
]);
return $work($payload);
} catch (RecoverableTaskException $e) {
// 中文说明:把可恢复异常转换为状态信号,等待 retry 或 abort
$command = Fiber::suspend([
'state' => 'recoverable',
'message' => $e->getMessage(),
]);
if ($command !== 'retry') {
throw $e;
}
}
}
});
}
public function start(): array
{
if ($this->fiber->isStarted()) {
throw new LogicException('任务已经启动');
}
return $this->capture(fn (): mixed => $this->fiber->start());
}
public function resume(mixed $value = null): array
{
$this->assertSuspended('恢复');
return $this->capture(fn (): mixed => $this->fiber->resume($value));
}
public function inject(Throwable $exception): array
{
$this->assertSuspended('注入异常');
return $this->capture(fn (): mixed => $this->fiber->throw($exception));
}
public function result(): mixed
{
if ($this->failure !== null) {
// 中文说明:保留原异常,避免把失败伪装成正常返回
throw $this->failure;
}
if (!$this->fiber->isTerminated()) {
throw new LogicException('任务尚未结束');
}
return $this->fiber->getReturn();
}
public function isFinished(): bool
{
return $this->fiber->isTerminated();
}
private function assertSuspended(string $action): void
{
if (!$this->fiber->isSuspended()) {
throw new LogicException("任务当前不能{$action}");
}
}
private function capture(callable $operation): array
{
try {
$signal = $operation();
return [
'state' => $this->fiber->isTerminated() ? 'done' : 'suspended',
'signal' => $signal,
];
} catch (Throwable $e) {
// 中文说明:未被 Fiber 内部处理的异常从切换调用处冒出
$this->failure = $e;
return ['state' => 'failed', 'error' => $e];
}
}
}
这里的 capture() 很关键。官方文档说明,如果 Fiber 回调在挂起前或恢复后抛出未捕获异常,该异常会从 start()、resume() 或 throw() 冒出。包装类把它保存为失败状态,但不会吞掉原对象;调用 result() 时仍抛出同一个异常。

启动与恢复:让业务层只处理信号
包装后,调用方不再直接判断该用 start() 还是 resume()。首次启动得到 waiting 信号,再把任务数据送入挂起点:
$payload['id'],
'status' => 'completed',
];
}
);
$startState = $task->start();
if (($startState['signal']['state'] ?? null) === 'waiting') {
// 中文说明:只有看到 waiting 信号后才恢复 Fiber
$task->resume(['id' => 42]);
}
$result = $task->result();
我比较喜欢让包装器返回结构化状态,而不是只返回 Fiber 的原始值。这样业务层能区分“刚挂起”“已经完成”和“未捕获失败”。代价是多了一层数组协议;规模较大的项目可以把它替换为只读 DTO 或枚举,减少字符串拼写错误。
把异常注入挂起点,并决定是否恢复
Fiber::throw($exception) 不是在 Fiber 外随便记录一个错误,而是恢复 Fiber,并让该异常从当前 Fiber::suspend() 调用处抛出。只有任务内部在对应边界捕获它,才有机会转成可恢复状态。
start();
// 中文说明:异常会从当前 suspend() 位置抛入 Fiber
$recoverable = $task->inject(
new RecoverableTaskException('临时资源尚未准备好')
);
if (($recoverable['signal']['state'] ?? null) === 'recoverable') {
// 中文说明:retry 让 Fiber 回到下一次 waiting 挂起点
$waiting = $task->resume('retry');
if (($waiting['signal']['state'] ?? null) === 'waiting') {
$task->resume(['id' => 42]);
}
}
$result = $task->result();
这个例子里的“恢复”包含两个决定。第一步,Fiber 内部只捕获 RecoverableTaskException,其他异常继续终止任务;第二步,调用方看到 recoverable 后明确发送 retry。如果传入其他命令,原异常重新抛出,包装器会记录为 failed。
这样做比无条件重试更稳妥:任务内部负责判断异常能否恢复,调度方负责判断当前是否应该重试。两者都不需要猜测对方的策略。

三个容易踩到的状态坑
重复调用 start
一个 Fiber 只能启动一次。首次 start() 后,即使它马上挂起,再次调用 start() 也会触发 FiberError;继续执行应该改用 resume()。包装器用 isStarted() 提前把错误转换成更贴近业务的 LogicException。
在非挂起状态 resume 或 throw
未启动、正在运行或已经终止的 Fiber 都不能被恢复或注入异常。外层通常只会在 Fiber 把控制权交回来以后调用这些方法,因此 isSuspended() 是最直接的门禁。
失败终止后读取 getReturn
getReturn() 只返回 Fiber 回调正常返回的值。Fiber 尚未终止,或者因为未捕获异常而终止时,调用它都会得到 FiberError。包装类先检查保存的 $failure,再检查 isTerminated(),避免把两类状态混为一谈。
| 当前状态 | 允许的主要操作 | 应避免的操作 |
|---|---|---|
| 未启动 | start() | resume()、throw()、getReturn() |
| 已挂起 | resume()、throw() | 再次 start() |
| 正常终止 | getReturn() | 继续恢复 |
| 异常终止 | 读取已保存异常 | getReturn() |
Fiber 适合什么,不适合什么
Fiber 的优势是“完整调用栈可暂停”。挂起点可以位于 Fiber 回调的深层函数中,不必像 Generator 那样让每一层都暴露 yield。它很适合构建协作式任务、事件循环适配层或异步框架的等待抽象。
但 Fiber 不会自动并行,也不会自动让阻塞 I/O 变成非阻塞。没有事件循环、I/O 多路复用或明确调度器时,一个 Fiber 在执行阻塞数据库或网络调用,仍会阻塞当前 PHP 线程。本文包装类只管理状态与控制权,不能替代成熟异步框架。
另一个取舍是恢复策略。示例只支持一个可恢复异常和 retry 命令,便于看清机制。生产项目通常还需要重试次数、退避、取消、超时和可观测字段;这些应该放在调度层,而不是继续把条件堆进 Fiber 回调。
采用前的检查清单
- 运行环境是否为 PHP 8.1 或更高版本。
- 首次调用是否固定使用
start(),后续只在挂起状态使用resume()。 suspend()输出值和resume()输入值是否有明确协议。- 注入异常是否只发生在挂起状态,任务内部是否只捕获真正可恢复的异常。
- 未捕获异常是否在包装边界保留,而不是转换成伪成功结果。
- 是否只在正常终止后调用
getReturn()。 - 是否明确提供事件循环或调度器,而不是把 Fiber 当成线程。
对我来说,Fiber 真正好用的分界线不是能不能写出 suspend(),而是调用方是否还要理解每个底层状态。把启动、挂起、恢复、异常注入和返回值读取封装后,业务代码只处理任务信号,协作式暂停才开始具备可维护性。
相关问题
Fiber 和 Generator 的主要区别是什么?
Fiber 可以从完整调用栈的深层位置挂起,不要求中间每一层都传播 yield;两者都不是操作系统线程。
调用 Fiber::throw 后一定会终止任务吗?
不一定。异常会从当前 Fiber::suspend() 处抛出;如果 Fiber 内部捕获并再次挂起或返回,任务可以继续,否则异常会冒到外层并终止 Fiber。
isTerminated 为 true 就能调用 getReturn 吗?
还要确认 Fiber 是正常返回。因未捕获异常终止时,getReturn() 仍会抛出 FiberError。
Fiber 能让同步 HTTP 请求变成异步吗?
不能单独做到。还需要非阻塞 I/O 与负责恢复 Fiber 的事件循环或框架。
参考资料
- PHP Fiber 类:
https://www.php.net/manual/en/class.fiber.php - Fiber::suspend:
https://www.php.net/manual/en/fiber.suspend.php - Fiber::throw:
https://www.php.net/manual/en/fiber.throw.php - Fiber::getReturn:
https://www.php.net/manual/en/fiber.getreturn.php
select 的 default 分支为何容易制造忙等,应该怎样改
- 上一篇
- select 的 default 分支为何容易制造忙等,应该怎样改
- 下一篇
- 把请求截止时间完整传递到数据库与下游 HTTP 调用
-
- 文章 · php教程 | 3小时前 |
- PHP 8.5 迁移 PDO 驱动常量时要改哪些代码
- 162浏览 收藏
-
- 文章 · php教程 | 11小时前 | pdo · php教程 · php pdo 数组分组 fetchAll FETCH_GROUP FETCH_COLUMN
- PHP PDO FETCH_GROUP 和 FETCH_COLUMN 怎么组合分组结果
- 217浏览 收藏
-
- 文章 · php教程 | 13小时前 | web安全 · php session SameSite session_set_cookie_params
- PHP session_set_cookie_params 怎么配置 SameSite
- 105浏览 收藏
-
- 文章 · php教程 | 15小时前 |
- PHP stream_context_create 怎么设置 TLS 主机校验
- 460浏览 收藏
-
- 文章 · php教程 | 19小时前 | 异常处理 · PHP · php Fiber Fiber::resume Fiber::throw
- PHP Fiber 抛出异常后还能再次 resume 吗
- 481浏览 收藏
-
- 文章 · php教程 | 1天前 |
- PHP array_find 怎么同时取得命中的键和值
- 110浏览 收藏
-
- 文章 · php教程 | 1天前 |
- PHP json_validate 怎么只检查 JSON 而不构造数组
- 425浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 361次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 417次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 430次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 384次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 209次使用
-
- 物流异常件转派时如何保留原单号与处理时限
- 2026-09-20 276浏览
-
- Go 并发编程协程及调度机制详情
- 2022-12-30 312浏览
-
- 一文详解Golang协程调度器scheduler
- 2022-12-31 231浏览
-
- go语言中的协程详解
- 2022-12-29 129浏览
-
- golang协程与线程区别简要介绍
- 2023-01-01 443浏览

