PHP 8.1即将加入Fibers纤程特性。我用PHP 8.1的alpha版本做了一个项目。尝试用Fibers来做异步编程。过程中踩了很多坑。本文是我的实战踩坑记录。需要说明的是PHP 8.1还在开发中。最终API可能会有变化。

一、为什么关注Fibers

先说说我为什么关注Fibers。

PHP一直是同步阻塞的语言。虽然有各种扩展可以做异步。比如Swoole、ReactPHP等。但是这些都不是PHP原生的。需要安装扩展。或者改变编程方式。

PHP 8.1计划加入原生的Fibers特性。也就是纤程。纤程是一种轻量级的协程。可以在函数执行的过程中暂停。然后在需要的时候恢复。这样就可以用同步的写法。实现异步的效果。

我对这个特性很感兴趣。就用PHP 8.1的alpha版本。做了一个小项目。尝试用Fibers来做异步的HTTP请求。想看看原生纤程好不好用。结果踩了很多坑。本文就是这些坑的记录。

二、Fibers是什么

先简单介绍一下Fibers。

Fiber是PHP 8.1新增的一个类。可以创建一个纤程。纤程创建之后不会立即执行。需要调用start()方法才会开始执行。

在纤程的执行过程中。可以调用Fiber::suspend()方法暂停执行。把控制权交还给主程序。主程序可以在需要的时候调用resume()方法恢复纤程的执行。

纤程和线程不同。纤程是在用户态管理的。不是操作系统调度的。所以纤程的切换开销很小。可以创建很多个纤程。

纤程和生成器也不同。生成器只能在生成器函数里yield。而纤程可以在任何嵌套的函数里暂停。更灵活。

三、基本用法

Fibers的基本用法很简单。

创建一个Fiber对象。传入一个回调函数。然后调用start()开始执行。在回调函数里。可以调用Fiber::suspend()暂停。主程序调用resume()恢复。

比如下面这个例子。创建一个纤程。在纤程里输出1。然后暂停。主程序输出2。然后恢复纤程。纤程输出3。

最终的输出顺序是1、2、3。因为纤程在输出1之后暂停了。主程序输出2。然后恢复纤程输出3。

这个例子很简单。但是展示了纤程的核心能力。就是可以在函数执行的中间暂停和恢复。

四、踩过的坑

了解了基本用法之后。我就开始用Fibers做异步HTTP请求。结果踩了很多坑。

坑1:PHP的函数大多是阻塞的

这是最大的坑。我以为有了Fibers就可以做异步了。结果发现。PHP的大部分函数都是阻塞的。比如filegetcontents、curl_exec、sleep等。这些函数在执行的时候。会阻塞整个进程。纤程无法暂停。

比如我在纤程里调用了filegetcontents去请求一个URL。这个函数会阻塞。直到请求完成。这时候其他纤程也无法执行。因为整个进程都被阻塞了。

所以Fibers本身并不能让阻塞的函数变成异步的。它只是提供了暂停和恢复的机制。要真正实现异步。还需要非阻塞的IO。比如用stream的非阻塞模式。或者用事件循环。

我最开始不知道这个。以为在纤程里调用curl就是异步的了。结果发现和同步没有区别。后来才明白。Fibers只是基础。需要配合非阻塞IO才能实现真正的异步。

坑2:需要自己实现事件循环

因为PHP没有内置的事件循环。所以用Fibers做异步的时候。需要自己实现事件循环。或者用第三方的事件循环库。

我最开始想自己实现一个简单的事件循环。用stream_select来监听多个socket。当某个socket有数据的时候。恢复对应的纤程。结果发现写一个可靠的事件循环很复杂。要处理各种边界情况。

后来我用了ReactPHP的事件循环。把Fibers和ReactPHP的事件循环结合起来。才实现了真正的异步HTTP请求。

但是这样一来。其实和直接用ReactPHP的Promise差不多了。Fibers只是让写法更像同步而已。

坑3:异常处理很麻烦

纤程里的异常处理也很麻烦。如果纤程里抛出了异常。异常会在resume()调用的地方抛出。如果没有捕获。就会导致整个进程崩溃。

而且纤程暂停之后。如果主程序没有恢复它。纤程就一直处于暂停状态。里面的资源也不会释放。比如打开的文件句柄。数据库连接。这些都需要手动处理。

我在项目里就遇到了这个问题。有些纤程因为异常没有被恢复。导致数据库连接没有释放。连接池很快就满了。

解决方法是。在创建纤程的时候。就要确保有完善的异常处理。不管纤程是正常完成还是异常结束。都要清理资源。可以用try-finally来保证资源释放。

坑4:调试困难

纤程的调试也很困难。因为执行流程不是线性的。在多个纤程之间切换。调用栈很复杂。

比如你在纤程A里暂停了。然后执行纤程B。这时候的调用栈既有主程序的。又有纤程B的。出了问题很难追踪。

而且PHP的调试工具对Fibers的支持还不好。Xdebug在alpha版本里不能很好地调试纤程。vardump和debugbacktrace的输出也很混乱。

我在调试的时候。只能靠大量的日志。在每个纤程的开始、暂停、恢复、结束的地方都打日志。然后通过日志来追踪执行流程。非常痛苦。

坑5:错误信息不清晰

alpha版本的错误信息也不清晰。有时候纤程出了问题。只报一个很笼统的错误。比如"Fiber is already suspended"。不知道是哪里出了问题。

有一次我调用resume()的时候报错。说纤程已经结束了。但是我明明没有让它结束。查了半天才发现。是纤程里的一个函数抛出了异常。导致纤程提前结束了。但是错误信息没有告诉我这个。

希望正式版能改进错误信息。让调试更容易一些。

坑6:和现有代码的兼容性

Fibers和现有代码的兼容性也有一些问题。比如有些扩展假设PHP是同步执行的。在纤程里调用可能会出问题。

我在项目里用了一个数据库扩展。在纤程里调用查询的时候。偶尔会出现奇怪的错误。后来发现是这个扩展不支持在纤程里使用。因为它的内部实现依赖了全局状态。纤程切换的时候全局状态会混乱。

解决方法是。在纤程里不要用不兼容的扩展。或者给这些扩展加锁。保证同一时间只有一个纤程在使用。

坑7:性能不如预期

我原本以为Fibers的性能会很好。因为纤程切换的开销很小。但是实际测试下来。性能不如预期。

原因是。虽然纤程切换的开销小。但是PHP本身的函数调用开销大。而且事件循环也有开销。在高并发的情况下。性能和Swoole比还是有差距。

当然。Fibers的优势不是性能。而是用同步的写法实现异步。让代码更容易写和维护。如果追求极致性能。还是用Swoole或者其他异步框架更好。

五、和其他异步方案的对比

用了Fibers之后。我对比了一下PHP的几种异步方案。

Swoole: 性能最好。功能最完善。有协程、异步IO、连接池等。但是需要安装扩展。而且编程方式和普通PHP有些不同。

ReactPHP: 基于事件循环的异步框架。用Promise的写法。不需要安装扩展。但是写法和普通PHP不同。需要适应。

Fibers: PHP原生支持。写法和同步代码差不多。但是需要自己实现事件循环。生态还不完善。性能不如Swoole。

我的看法是。如果是新项目。追求性能。用Swoole。如果不想装扩展。可以用ReactPHP。Fibers目前还不太适合生产环境。等生态完善了再考虑。

六、给想尝试Fibers的人的建议

如果你也想尝试Fibers。我有几个建议。

1. 等正式版

现在还是alpha版本。API可能会变。bug也比较多。建议等正式版发布之后再用在生产环境。现在可以先在测试环境体验一下。

2. 理解异步的本质

Fibers只是提供了暂停和恢复的机制。要实现真正的异步。还需要非阻塞IO和事件循环。不要以为用了Fibers就自动异步了。

3. 做好异常处理

纤程里的异常处理很重要。一定要完善。确保资源能正确释放。否则很容易出现资源泄漏。

4. 做好测试

纤程的执行流程复杂。容易出bug。一定要做好测试。覆盖各种边界情况。比如纤程没有恢复、纤程异常、纤程嵌套等。

5. 不要盲目追新

Fibers是一个新特性。但是不是所有项目都需要。如果你的项目是普通的Web应用。用同步的方式就够了。不需要用Fibers。只有在需要高并发IO的时候。才考虑用异步方案。

七、写在最后

PHP的Fibers是一个很有前景的特性。它让PHP可以用同步的写法实现异步。降低了异步编程的门槛。

但是目前它还在开发中。生态还不完善。坑也比较多。不太适合生产环境。等正式版发布。生态完善之后。应该会有很好的发展。

我这次踩坑的经历。让我对Fibers有了更深的理解。也期待正式版的发布。

最后用一句话结束本文:"新特性需要时间成熟。耐心等待也是一种智慧。"愿PHP的异步生态越来越好。让开发者有更多的选择。