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的异步生态越来越好。让开发者有更多的选择。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录