PHP一直顶着“同步阻塞”的帽子,这帽子戴得久了,很多人就觉得PHP天生干不了并发的事儿。确实,在传统的PHP-FPM模式下,每个请求独占一个进程,从头到尾一条线走完,中途要是碰上慢吞吞的远程API调用或者复杂的数据库查询,整个进程就只能干等着。后来有了Swoole、ReactPHP这类扩展和库,局面才有所改观,但它们要么需要额外安装扩展,要么彻底改变了程序的运行方式,对不少存量项目来说改造成本不低。
PHP 8.1带来的Fiber(纤程),提供了一种完全不同的思路。它既不是扩展,也没有改变PHP的同步运行模型,而是在语言层面给了开发者一个“可中断的函数”能力——你可以让一个函数在执行到某个点的时候主动让出,先去干别的事儿,过一会儿再回来接着执行。这个特性用好了,就能在传统的同步代码里挖出一些并发操作的空间,而不必重写整个应用。
一、Fiber解决了什么问题
先想象一个很常见的场景:某个页面需要展示三部分数据,分别来自三个不同的远程服务。传统写法是依次发请求,第一个响应回来才发第二个,总耗时等于三个请求时间的总和。假如每个请求平均200毫秒,页面至少600毫秒才能吐出来。
聪明一点的写法会用curl_multi_exec把多个请求同时发出去,等全部回来再一起处理。这种做法确实能并行,但代码写起来相当繁琐——你得手动管理多个句柄,拼装结果,异常处理也容易漏。而且curl_multi只能并行HTTP请求,如果你的任务里既有HTTP又有数据库查询,它就没招了。
Fiber的着眼点不同。它允许你在编写代码时,用一种接近同步的风格来描述异步操作的流程。一个Fiber内部的代码可以被手动“暂停”和“恢复”,暂停的时候,控制权回到调用方,调用方可以去启动其他Fiber或者做别的事。这样一来,我们就可以手动调度多个看起来是同步、实际上可以被中断的任务片段,让它们交错执行,在等待IO的间隙处理其他任务。
不过要明确一点:Fiber本身并不提供非阻塞IO。它只是一个控制流工具,真正让并发得以实现,还得配合能提供异步能力的驱动——比如curl_multi、stream_select,或者ReactPHP的事件循环。Fiber的价值在于让你把这些异步底层包装成同步风格的代码,极大降低心智负担。
二、Fiber的基本用法——一个可控的中断函数
直接看代码最容易理解。下面创建一个Fiber,它内部会“暂停”两次,每次返回一个值,调用方可以决定何时恢复它。
$fiber = new Fiber(function() {
echo "Fiber开始n";
$value1 = Fiber::suspend('第一次挂起');
echo "收到恢复值1:{$value1}n";
$value2 = Fiber::suspend('第二次挂起');
echo "收到恢复值2:{$value2}n";
return '最终结果';
});
echo "启动Fibern";
$result1 = $fiber->start();
echo "start返回:{$result1}n";
$result2 = $fiber->resume('外部数据A');
echo "resume第一次返回:{$result2}n";
$result3 = $fiber->resume('外部数据B');
echo "resume第二次返回:{$result3}n";
这段代码的执行顺序符合直觉但又稍显绕脑。调用start()后,Fiber内部开始运行,直到遇见第一个suspend,把“第一次挂起”这个字符串作为start()的返回值抛出,然后Fiber暂停。此时外部代码继续执行,处理完一些事情后调用resume('外部数据A'),这个字符串就变成了Fiber内部suspend的返回结果,赋给$value1,然后Fiber继续往下走。遇到第二个suspend时,再次抛出返回值并暂停,直到下一次resume。最终return的值由最后一次resume返回。
这个机制看起来像是一个加强版的生成器,但比生成器更灵活——Fiber允许从任意位置恢复,而生成器只能从上次yield的位置继续。更重要的是,Fiber的暂停可以发生在调用栈的任意深度,你可以在嵌套函数里调用Fiber::suspend(),而生成器的yield只能出现在第一层。
三、实战:用Fiber并发处理多个HTTP请求
理论总是干巴巴的,咱们直接上硬菜。假设我们需要同时请求三个不同的天气服务接口,拿到各自的数据后汇总返回。使用Fiber配合curl_multi,可以写出非常清晰的代码。
首先设计一个“任务调度器”的雏形。它维护一个任务队列,每次循环检查哪些IO操作已经就绪,然后恢复对应的Fiber。
class AsyncScheduler {
private $tasks = [];
private $curlMulti;
public function __construct() {
$this->curlMulti = curl_multi_init();
}
public function addTask(Fiber $fiber) {
$this->tasks[] = $fiber;
}
public function run() {
while (!empty($this->tasks) || $this->hasActiveCurl()) {
// 调度未启动或可恢复的Fiber
foreach ($this->tasks as $index => $fiber) {
if ($fiber->isSuspended()) {
$fiber->resume();
} elseif (!$fiber->isStarted()) {
$fiber->start();
}
// 如果Fiber完成,移出队列
if ($fiber->isTerminated()) {
unset($this->tasks[$index]);
}
}
// 等待curl事件
if ($this->hasActiveCurl()) {
curl_multi_select($this->curlMulti);
curl_multi_exec($this->curlMulti, $active);
}
}
}
private function hasActiveCurl() {
return count(curl_multi_getcontent($this->curlMulti)) > 0;
}
public function __destruct() {
curl_multi_close($this->curlMulti);
}
}
这个调度器还很粗糙,但已经能跑通基本流程。接下来定义一个可以用Fiber包裹的异步HTTP请求函数,它把curl_multi的句柄注册进去,然后在等待网络响应时暂停Fiber,把控制权交还给调度器。
function asyncFetch(string $url, AsyncScheduler $scheduler): mixed {
$fiber = Fiber::getCurrent(); // 获取当前运行的Fiber
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
// 把curl句柄加入multi
curl_multi_add_handle($scheduler->curlMulti, $ch);
// 注册一个回调,当请求完成时恢复Fiber
$callback = function() use ($fiber, $ch) {
$response = curl_multi_getcontent($ch);
curl_multi_remove_handle($fiber->scheduler->curlMulti, $ch);
curl_close($ch);
// 这里要用resume并传入结果,但无法直接访问fiber,需要调整设计
};
// 简化处理:直接在挂起后由调度器轮询
Fiber::suspend(); // 挂起,等待IO完成
// 恢复后,获取结果
$response = curl_multi_getcontent($ch);
curl_multi_remove_handle($scheduler->curlMulti, $ch);
curl_close($ch);
return $response;
}
上面的asyncFetch逻辑其实有个鸡生蛋的问题:Fiber挂起后,调度器需要知道这个Fiber关联了哪个curl句柄,才能在有数据时恢复它。这就需要更精细的管理——把Fiber和其IO事件绑定。
一个更成熟的做法是,每个Fiber挂起时返回一个描述“我正在等待什么事件”的结构,调度器根据这个结构把Fiber加入对应的事件监听列表。当事件触发时,调度器再恢复对应的Fiber。这个模式在ReactPHP等库里已经广泛使用,我们这里不做那么复杂,而是用一个简易的轮询方式:Fiber挂起后,调度器反复调用curl_multi_exec,检查哪个句柄已经完成,然后恢复对应的Fiber。
为了能关联起来,可以让每个Fiber在挂起前把它自身的引用和curl句柄注册到调度器中。下面给出一个简化但可直接运行的完整示例,使用全局状态来管理。
// 全局调度状态(简化演示用)
$curlMulti = curl_multi_init();
$waitingFibers = []; // [curlHandle => Fiber]
function asyncGet(string $url): string {
global $curlMulti, $waitingFibers;
$fiber = Fiber::getCurrent();
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_multi_add_handle($curlMulti, $ch);
// 记录句柄与Fiber的映射
$waitingFibers[(int)$ch] = $fiber;
// 挂起,等待调度器在IO就绪时恢复
Fiber::suspend();
// 恢复后获取结果
$result = curl_multi_getcontent($ch);
curl_multi_remove_handle($curlMulti, $ch);
curl_close($ch);
unset($waitingFibers[(int)$ch]);
return $result;
}
// 调度循环
function runFibers(Fiber ...$fibers) {
global $curlMulti, $waitingFibers;
$activeFibers = $fibers;
while (!empty($activeFibers) || count($waitingFibers) > 0) {
// 启动或恢复每个Fiber
foreach ($activeFibers as $idx => $fiber) {
if ($fiber->isTerminated()) {
unset($activeFibers[$idx]);
continue;
}
if (!$fiber->isStarted()) {
$fiber->start();
} elseif ($fiber->isSuspended()) {
$fiber->resume();
}
}
// 运行curl的IO循环
curl_multi_exec($curlMulti, $stillRunning);
if ($stillRunning) {
curl_multi_select($curlMulti);
}
// 检查是否有已完成的curl句柄,如果有则恢复对应Fiber
while ($done = curl_multi_info_read($curlMulti)) {
$handle = $done['handle'];
$handleId = (int)$handle;
if (isset($waitingFibers[$handleId])) {
$f = $waitingFibers[$handleId];
// 该Fiber会从恢复点继续执行
// 但因为我们是在循环里统一resume,这里不需要额外动作
}
}
}
curl_multi_close($curlMulti);
}
// 实际任务:并发请求三个URL
$fiber1 = new Fiber(function() {
$data = asyncGet('https://jsonplaceholder.typicode.com/todos/1');
echo "任务1完成: " . substr($data, 0, 50) . "n";
});
$fiber2 = new Fiber(function() {
$data = asyncGet('https://jsonplaceholder.typicode.com/todos/2');
echo "任务2完成: " . substr($data, 0, 50) . "n";
});
$fiber3 = new Fiber(function() {
$data = asyncGet('https://jsonplaceholder.typicode.com/todos/3');
echo "任务3完成: " . substr($data, 0, 50) . "n";
});
echo "开始并发请求...n";
$start = microtime(true);
runFibers($fiber1, $fiber2, $fiber3);
echo "总耗时: " . (microtime(true) - $start) . " 秒n";
这个代码虽然用了全局变量(实际项目里可以封装成类),但逻辑很清晰:每个Fiber在发起HTTP请求后主动挂起,调度器轮询curl状态,一旦某个请求完成,Fiber就被恢复并处理响应。三个请求是并发发出的,总耗时接近于最慢的那个请求所用时间,而非三个请求之和。
四、这个方案的局限和适用场景
必须承认,上面这套手写的调度器还远谈不上生产级。它没有合理的错误处理,如果一个请求失败,Fiber可能永远恢复不了;也没有超时控制,万一某个接口长时间无响应,整个调度循环都会卡住。而且,curl多句柄的管理本身就比较复杂,内存占用也比单次请求高。
更关键的是,Fiber只能在CLI模式下发挥最大价值。在PHP-FPM中,每个请求的生命周期很短暂,Fiber的并发调度意义有限——因为FPM进程本身就是独立的,多个请求之间无法共享Fiber调度。除非你用Swoole或ReactPHP这种常驻内存的运行环境,Fiber才会真正发光发热。事实上,Swoole的协程和ReactPHP的Promise都已经在底层用上了Fiber(或类似机制)来让异步代码同步化。
那么,Fiber在我们普通开发者的日常里到底有什么用?我觉得它的最大价值在于思维方式的转变:它让你意识到,PHP代码的执行流是可以被暂停和恢复的。当你面对一些需要“等待”的操作时(不只是HTTP请求,还包括文件读写、数据库查询、进程等待等),你可以开始考虑用一种更高效的编排方式去组织它们,而不是简单地从上往下顺序执行。
另外,Fiber在写测试代码时也特别有用。比如模拟一个需要多次交互的复杂流程,可以通过控制Fiber的暂停和恢复来注入特定状态,让测试代码更加灵活。
五、写在最后
PHP的生态一直在稳中求变,Fiber的引入不是要颠覆什么,而是在语言内核里埋下一颗种子——让开发者可以在不离开PHP舒适区的前提下,尝试更高效的执行模型。如果你正在维护一个老项目,并且里面有不少可以并行处理的独立IO操作,不妨抽一个小模块用Fiber试试看,用几十行代码把那些串行等待变成并发执行,带来的性能提升往往超出预期。
当然,如果项目已经重度依赖Swoole这类异步框架,那Fiber对你来说可能只是个底层实现细节,不需要直接打交道。但对还在传统同步PHP里打转的朋友来说,Fiber是一扇值得推开的门,门后不是全新的世界,而是一个让你现有的代码跑得更快的可能性。

