Fibers(纤程)是PHP 8.1引入的新特性,它提供了一种轻量级的并发机制,让PHP也能像Go和Node.js一样做异步编程。我对这个特性非常感兴趣,花了不少时间学习和实践,但是最后在大部分场景下选择了放弃。本文记录了我学习和使用Fibers的完整经历,包括它的原理、优势、坑点、以及为什么我最终选择了放弃。如果你也在考虑用Fibers,希望这篇文章能给你一些参考。

一、初识Fibers:充满期待

先说说我是怎么开始关注Fibers的。

PHP一直以来都是同步阻塞的模型,一个请求一个进程或者线程,遇到IO操作就等着。这种模型简单稳定,但是并发能力有限,遇到大量IO操作的时候,性能上不去。

后来有了Swoole和Workerman这些扩展,让PHP也能做异步和协程,但是这些都是第三方扩展,不是PHP原生的,而且学习成本比较高,和传统的PHP编程方式差异很大。

PHP 8.1引入了原生的Fibers,也就是纤程。它是一种比线程更轻量级的并发单位,可以在用户态进行调度,不需要操作系统的参与。一个Fiber的创建和切换开销非常小,可以同时创建成千上万个。

看到这个消息的时候我很兴奋。因为这意味着PHP原生支持了纤程,以后做高并发的IO密集型应用,不需要再依赖第三方扩展了。而且Fibers的API看起来很简单,比Swoole的协程容易上手多了。

于是我开始学习Fibers的用法,并且在项目中尝试使用。刚开始的时候觉得很美好,但是用着用着,问题就来了。

二、Fibers的基本原理和用法

先简单介绍一下Fibers的基本原理和用法。

Fiber是一种可以暂停和恢复的代码块。你可以创建一个Fiber,然后启动它。Fiber运行到某个点的时候,可以主动暂停(suspend),把控制权交还给调用者。调用者可以在适当的时候恢复(resume)这个Fiber,让它从暂停的地方继续执行。

这个机制和协程很像,但是Fibers更底层,它本身不提供调度器,只是提供了暂停和恢复的原语。你需要自己写调度逻辑,或者用基于Fibers的框架(比如ReactPHP、Amp等)。

一个简单的例子:

$fiber = new Fiber(function() {
    echo "Fiber开始\n";
    Fiber::suspend();
    echo "Fiber恢复\n";
});

echo "启动Fiber\n";
$fiber->start();
echo "Fiber已暂停\n";
$fiber->resume();
echo "Fiber结束\n";

输出是: 启动Fiber Fiber开始 Fiber已暂停 Fiber恢复 Fiber结束

可以看到,Fiber在suspend的地方暂停了,调用者做了一些事情之后,再resume让它继续执行。

这个机制可以用来做异步IO。比如,发起一个网络请求之后,不需要等它返回,可以先暂停,去做别的事情,等网络请求返回了再恢复继续执行。这样一个进程就可以同时处理多个IO操作,大大提升并发能力。

听起来很美好对吧?但是实际用起来,问题比想象中多。

三、Fibers的优势

先说Fibers的优势。

优势1:轻量级

Fiber非常轻量,创建和切换的开销很小。一个Fiber只需要几KB的内存,可以同时创建几十万个。相比之下,进程和线程的开销就大得多了。

这意味着,用Fibers可以实现很高的并发,尤其是在IO密集型的场景下。

优势2:原生支持

Fibers是PHP原生的特性,不需要安装第三方扩展。只要你的PHP版本是8.1以上,就可以直接用。这降低了使用门槛,也避免了第三方扩展的兼容性问题。

优势3:API简单

Fibers的API非常简单,只有几个方法:start、resume、suspend、isStarted、isRunning、isSuspended、isTerminated。学习成本很低,看一下文档就能上手。

优势4:和同步代码兼容

Fibers的代码看起来和普通的同步代码差不多,不需要用回调或者Promise的方式写。你可以在Fiber里写正常的顺序代码,遇到需要等待的地方suspend一下就好了。这比传统的回调地狱要好很多。

四、遇到的问题和坑

说了优势,再说说我遇到的问题和坑。这也是我从入门到放弃的原因。

问题1:没有内置调度器

这是最大的问题。Fibers只提供了暂停和恢复的原语,没有内置的调度器。也就是说,你需要自己写代码来管理Fiber的创建、暂停、恢复和销毁。

比如,你想同时发起10个HTTP请求,然后等它们都返回。你需要自己创建10个Fiber,每个Fiber里发起一个请求然后suspend,然后你需要监听所有请求的返回,哪个先返回就resume哪个。这个调度逻辑写起来还是挺复杂的。

当然,你可以用ReactPHP或者Amp这些基于Fibers的框架,它们提供了调度器和事件循环。但是这样的话,你就不是单纯用Fibers了,而是在用一个完整的异步框架,学习成本和改造成本都不低。

问题2:生态不成熟

Fibers是PHP 8.1才引入的新特性,到现在(2021年初)还比较新,生态还不成熟。很多常用的库和框架还不支持Fibers,比如PDO、cURL这些扩展,默认还是阻塞的,在Fiber里用它们,还是会阻塞整个进程。

你可能会说,可以用异步的客户端啊。但是异步的PDO和cURL客户端都不成熟,功能也不完善,很多特性不支持。而且很多第三方SDK都是同步阻塞的,在Fiber里用不了。

这就导致,你想用Fibers做异步,但是很多常用的功能都没有对应的异步实现,用起来非常别扭。

问题3:调试困难

Fibers的调试比普通代码困难很多。因为Fiber可以暂停和恢复,执行流程不是线性的,出了问题很难追踪。

比如,一个Fiber暂停了,然后过了很久才恢复,这中间发生了什么,很难追溯。而且多个Fiber交错执行,调用栈也很复杂,调试器的支持也不好。

我有一次遇到一个bug,在Fiber里某个变量的值不对,但是同样的代码在普通函数里就是对的。我调试了很久才发现,是因为Fiber暂停的时候,另一个Fiber修改了共享的变量。这种并发相关的bug,在单线程同步代码里是不会出现的,但是用了Fibers之后就需要考虑了。

问题4:错误处理复杂

Fibers的错误处理也比较复杂。Fiber里抛出的异常,如果没有被捕获,会在resume的时候抛到调用者那里。这就意味着,你不仅要在Fiber内部处理异常,还要在调用resume的地方处理异常。

而且如果一个Fiber抛出异常终止了,你需要检查它的状态,做相应的清理工作。如果有多个Fiber,其中一个出了问题,其他的怎么处理,是继续还是取消,这些都需要自己设计。

问题5:性能提升有限

这是最让我失望的一点。我原本以为用了Fibers之后,性能会有质的提升。但是实际测试下来,在大部分场景下,性能提升有限。

原因是,PHP的很多扩展和函数还是阻塞的,在Fiber里用它们,还是会阻塞。除非你全部用异步的客户端,但是异步客户端的生态又不成熟。

而且对于CPU密集型的任务,Fibers根本没用,因为Fibers是协作式的,一个Fiber在跑CPU密集型任务的时候,其他Fiber都得等着。这时候还不如用多进程。

只有在纯IO密集型,而且所有IO都有异步实现的场景下,Fibers才能发挥出优势。但是这种场景在实际项目中并不多。

问题6:团队学习成本

虽然Fibers的API简单,但是要用好它,需要理解异步编程的思想,需要处理并发、调度、错误处理等问题。对于习惯了同步编程的PHP开发者来说,这个转变还是挺大的。

我们团队大部分人都是写同步PHP代码出身的,对异步编程不熟悉。如果项目里大量使用Fibers,团队的学习成本会很高,而且容易出bug。考虑到团队的实际情况,我们觉得还是用传统的同步模型更稳妥。

五、从推广到放弃

和我之前用Serverless的经历类似,我对Fibers的态度也经历了一个从热情到理性的过程。

刚开始的时候,我觉得Fibers是PHP的未来,在团队里积极推广,想把项目都迁到Fibers架构上。但是随着使用的深入,遇到的问题越来越多,我开始重新评估Fibers的适用场景。

我发现,我们的大部分项目,其实并不需要Fibers。我们的项目大部分是传统的Web应用,每个请求的逻辑不复杂,IO操作也不多,用传统的同步模型完全够用。而且同步模型简单稳定,团队熟悉,维护成本低。

只有少数几个项目,有大量的并发IO需求,比如爬虫、数据同步、消息推送等。这些项目用Fibers确实能提升性能,但是这些项目数量不多,而且可以用Swoole或者多进程来解决,不一定非要用Fibers。

所以我最后的决定是:大部分项目继续用传统的同步模型,只有少数有特殊需求的项目,评估之后再决定是否用Fibers。不再盲目推广Fibers,而是根据项目的实际需求来选择技术方案。

这个决定不是对Fibers的否定,而是对它有了更清醒的认识。Fibers是一个好特性,但是它不是银弹,不是所有项目都适合用。

六、哪些场景适合用Fibers

虽然我在大部分场景下放弃了Fibers,但是在一些特定场景下,它还是很有用的。

1. 高并发IO密集型应用

比如爬虫、API聚合服务、微服务网关等,这些应用需要同时发起大量的网络请求,用Fibers可以大大提升并发能力和吞吐量。

2. 实时应用

比如聊天室、实时通知、游戏服务器等,这些应用需要维护大量的长连接,用Fibers可以在一个进程里处理大量连接,不需要为每个连接开一个进程或者线程。

3. 异步任务处理

比如需要同时处理多个异步任务,等待它们都完成之后再继续。用Fibers可以让代码写起来像同步一样,但是实际上是异步执行的。

4. 学习和研究

即使不在生产环境用,学习Fibers也能帮助你理解异步编程和协程的原理,提升你的技术视野。

七、给想尝试Fibers的朋友的建议

如果你也想尝试Fibers,这里有一些建议。

1. 先想清楚需求

不要因为Fibers是新特性就盲目使用。先想清楚你的项目是否真的需要高并发的异步处理。如果只是普通的Web应用,传统的同步模型就够了,没必要引入Fibers的复杂度。

2. 从小项目开始

不要一上来就把核心项目迁到Fibers。先从一个简单的、非核心的小项目开始尝试,积累经验,了解Fibers的优势和坑。等熟悉了之后,再考虑在更多项目中使用。

3. 考虑用成熟的框架

不要自己写调度器,太复杂了。可以考虑用ReactPHP、Amp这些基于Fibers的成熟框架,它们提供了完善的事件循环、调度器、异步客户端等。虽然学习成本高一些,但是比自己造轮子靠谱。

4. 做好错误处理和监控

异步编程的错误处理比同步复杂,一定要做好异常捕获和错误处理。同时要加好监控,出了问题能及时发现和定位。

5. 保留回退方案

在使用Fibers的同时,保留同步版本的实现。如果发现Fibers不适合你的项目,可以快速回退,不要被锁死在Fibers架构上。

八、写在最后

从入门到深入,再到大部分场景下放弃,这段Fibers的实践经历让我收获很多。

我不再是那个盲目追新的技术狂热者了。我学会了理性地看待每一项新技术,看到它的优势,也看到它的局限。没有完美的技术,只有适合的技术。

Fibers是PHP的一个重要特性,它让PHP在异步编程的道路上迈出了重要的一步。但是它还比较新,生态还不成熟,还需要时间来发展和完善。也许未来某一天,Fibers的生态成熟了,它会成为PHP异步编程的主流。但是在那一天到来之前,我们还是要根据实际情况,做出理性的选择。

希望我的经历能给正在考虑用Fibers的朋友一些参考。如果你也有类似的经历,欢迎在评论区交流讨论。

最后用一句话结束本文:"新技术值得学习,但是否采用,要根据实际需求来决定。"愿每一个技术人都能理性看待新技术,做出最适合自己项目的选择。