最新的 PHP 8.1 增加了一个 Fiber 的提案,最近讨论的比较多。有不少好事者拿来说事儿,说是 “Fiber 进入内核之后,Swoole 的使用者就大幅减少“
实际上 Fiber 扩展进入内核后,由于它是一个非常底层的 API ,并不是直接可以使用的技术,不会对 Swoole 产生影响。真正和 Swoole 竞争的是应该是 Amphp 、ReactPHP 。Fiber 反而对 Swoole 是有好处的,PHP 内核开发者维护了协程切换的全局状态列表,Swoole PHPCoroutine 这部分的代码实现就变简单了。另外,其他扩展也会注意到协程的存在,使用 C 全局变量或栈上内存时考虑到协程切换的可能性,避免出现 Crash。ext-fiber 合并进来之后,也应标记为 alpha 状态,一些特殊情况能会引起崩溃,需要比较长的时间去收集解决这些问题。
最近这几年即便官方连续出了很多个大版本,PHP 还是一直是在走下坡路。有许多 PHP 开发者说是因为 PHP 性能不行,没有 JIT。于是 PHP8.0 加入了 JIT。还有人说 PHP 没有协程,所以 PHP8.1 要加入 Fiber。马上就会有人说 PHP 缺少多线程,按照现在这个节奏,可以预见未来有可能 PHP 的多线程扩展 parallels 也会合并到内核。PHP8 还加入了一个 FFI 模块,甚至可以直接使用 PHP 调用 C 库。
可是真的加入如此多的能力,PHP 就得到很大的改变了吗?
你们想要的 Fiber 是这样的:
实际上 PHP 8.1 Fiber 是这样的:
动态语言中除了 PHP 之外,Python、Ruby、Lua 在很早就有协程支持了,但实际上这些编程语言在协程并发编程方面并没有多出色。真正将协程技术发扬光大的是 Golang ,为什么 Golang 在协程编程方面的如此成功?这是因为它提供了完整的、成体系的一整套技术方案,从语言设计到编译器、协程调度器、标准库、调试器,这才是工业级的技术。在多线程技术方向,很多编程语言都有多线程支持,但真正被广泛使用、达到工业级水平的多线程系统只有 Java 。在 PHP 中真正能达到工业级水平的技术也就是 Apache+mod_php 和 PHP-FPM 。
协程的技术也是一样,PHP 开发者想要从传统的 LAMP/LNMP 短生命周期、串行编程的模式转型到 CSP 协程+通道并发编程,目前暂时也只有 Swoole 是相对来说最成熟的方案。用户真正需要的是一种完整的、系统性、成体系、简单易用、可靠的一整套技术方案。
PHP 8.1 加入 Fiber 我认为是一个仓促的决定。不如系统性地设计一下,从这些7个方面考虑:
- EventLoop API
- 协程(对应 ext-fiber)
- IO 调度器(Socket/FileSystem/ChildProcess/Signal/Timer/Stdout/Stdin)
- CPU 调度器
- 现有同步阻塞 IO 扩展(redis、curl、php_stream、sockets、mysqli、pdo_mysql 等)和内置函数(sleep、shell_exec、sleep、gethostbyname 等)如何实现支持协程,变成异步非阻塞模式
- 协程通信(channel)
- 服务器:实现 PHP-FPM 协程版,或者提供一个新的协程 HttpServer
事件循环
EventLoop 是协程实现中最核心的基础设施,这里不是指具体实现,C 层面 select/epoll/poll ,PHP 层面 stream_select 或者 libevent/libuv/event 扩展都可以实现,如果 ZendVM 底层提供了 EventLoop,那么不同的框架、不同的库可以在同一个 Loop 中,协程调度器也可以构建在此之上。如果没有统一的 EventLoop 的基础设施,amphp 、 reactphp 等框架都需要各自实现,意味着你在使用 amphp 的程序时,无法使用 reactphp 实现的任何类库。
Node.js、Golang、Swoole 底层都有一个全局的 EventLoop,所有 IO 行为都会被注册到 EventLoop 中,事件触发后执行 callback 或者调度协程。
阻塞 IO 函数
PHP 提供的很多 IO 操作函数都是阻塞的,如果在协程中发生阻塞,就会导致并发失效。退化成和普通 PHP-FPM 一样的串行模式。协程实现中必须要考虑到如何解决这个问题。
Amphp 和 ReactPHP 目前(2021年)采用的实现方式,是使用 PHP 代码实现基于协程的异步非阻塞 IO 版本,在 2018 年之前 Swoole 也是采用这个模式。这样做最大的问题是,
- 成本太高,无法复用 PHP 生态,重复造轮子,需要重新实现 Redis、MySQL、CURL、Http2、WebSocket、Kafka 等大量网络 IO 库
- 质量不高,不像同步阻塞的版本经过大规模验证
- 兼容性差,如果用户使用了一个第三方库,其中包含了阻塞 IO 的客户端调用,就前功尽弃了
- 学习成本高,用户需要学习一套全新的 API ,这对 PHP 开发者非常不友好
- PHP 实现的版本可能还会存在性能问题,在 Swoole 中由于是使用 C 实现的不存在这一点
所以 Swoole 在 4.1 版本(2018年)开始采用了全新的实现方式,会 Hook 掉 PHP 扩展中的函数指针,通过很少的工作量就彻底解决了这个问题。PHP 开发者直接使用同步阻塞客户端的 API 即可,底层会自动替换为非阻塞的协程版本。比如下面的代码:
<?php
Co\run(function () {
go(function (){
sleep(10);
});
go(function (){
sleep(2);
});
go(function (){
file_get_contents("https://www.baidu.com/");
});
});
Swoole 会替换 PHP 内置的 sleep 和 file_get_contents 函数,变成协程版本,上面的程序就变成了完全并发的了,对用户来说是无感知的。
CPU 调度器
由于 PHP 是动态解释执行的编程语言,在实现协程 CPU 调度器方面比 Golang 有优势。Golang 需要在编译器内做很多工作,控制单个协程占用的 CPU 时间,避免几个协程耗尽 CPU 资源。PHP 可以在 VM 层面直接实现中断,精准控制每个协程最大可执行的时间。
Golang 使用 GPM 模型解决了这个问题,如果一部分协程持续占用 CPU ,调度器会创建更多 Thread 执行新的任务,退化为操作系统调度。PHP 由于不支持多线程,暂时无法实现 GPM 模型,目前 Swoole 所采作用的 VM 中断调度实现是最优解
在 Swoole 的实现中,底层创建了一个中断线程,每 5ms 会产生一个中断信号,在中断函数中判断当前协程执行的时长,如果超过了规定的 10ms 最大执行时间,会自动让出 CPU 切换至其他可执行的协程。
<?php
Co::set(['enable_preemptive_scheduler' => true]);
Co\run(function () {
go(function (){
for($i=0; $i<10000000; $i++) {
if ($i % 10000 == 0) {
echo "Co 1\n";
}
}
});
go(function (){
for($i=0; $i<10000000; $i++) {
if ($i % 10000 == 0) {
echo "Co 2\n";
}
}
});
});
以上程序会交替执行,每个协程最大执行时间不超过 10ms
我认为正确的方式
创建多个 RFC ,把这些问题讨论清楚,在 PHP9 版本中提供完整的协程方案实现。不求做到 Golang 的程度,至少要能达到生产可用。这样 PHP 才会有大的改变。不过这可能就真要取代 Swoole 了 [哭笑]。
再介绍一下 Swoole 现在做到什么程度:
- 完整的协程+通道实现
- 提供了 CPU 调度器,即使是密集计算的程序,也可以使用协程,调度器会按照10ms时间片切换协程
- 支持绝大部分 PHP 的常用扩展和内置函数,LAMP 时代的代码可以不用修改直接 copy 到协程里运行,而且是异步非阻塞的方式,是真正的并发,我认为这才是黑科技,PHP 协程方案的关键技术
- curl 扩展也可以协程化,包括 curl 和 curl_multi,guzzle 可以直接用,腾讯云、阿里云的 PHP SDK 可以直接在协程中使用
- 提供了 PGSQL 协程实现,基于 pgsql 官方 C 库的异步 API 实现
- 提供了 ZooKeeper 协程实现,是基于官方的 C 库 插入了 Hook 代码实现
- Kafka 协程库的实现
- 支持协程的新一代调试器:yasd
- 支持 PHP7.2-8.0 所有版本,ext-fiber 只支持 8.0 以上版本
PHP 作为一个社区驱动的开源项目,背后没有商业公司支持,没有 Golang、Java、Node.js(v8) 这样充足的研发资源投入,需要依赖全世界各地的贡献者提交代码,在产品化方面还是做的不够好。国内有一些人一直在 diss Swoole 有商业公司,但正是因为有商业收入,才保证了我们在 Swoole 开源项目研发上的连续性,在产品化方面也会做的更好。
我对 Fiber 的担忧
Fiber 集成到 PHP 中之后,会有很多 PHP 的框架或者类库创建自己的协程方案,由于 PHP 只提供了 Coroutine Context 的实现,其他几个方面并没有提供,在 PHP 生态中将会出现很多流派的 EventLoop、AsyncIO 、NetworkClient 多种多样的实现。就拿 sleep 函数来说,现在 Amphp 和 ReactPHP 分别叫做 amp\delay 和 react\sleep 。
没有统一的标准,意味着社区的高度分裂。一旦确定了方向,技术的发展演进是非常快的,多样性是一件好事,但也会带来更多新问题,再想统一是很困难的一件事情。即便是 Symfony、Laravel 这样处于顶端的 PHP 框架,也不具备能够 100% 覆盖整个 PHP 生态的能力,这将走向失控。
基于 Swoole/Swow 的方案,实际上依旧是 PHP 原先的生态,大家使用的依然是最熟悉的那些 PHP 函数和库,异步编程和同步阻塞 IO 编程的生态是一致的。比如在 Swoole 协程中可以直接使用阿里云、腾讯云、AWS 提供的 SDK,Fiber 生态下情况就会比较复杂。
Swow 是一个从 Swoole 项目中剥离与协程无关特性,使用 Swoole 协程设计方案的全新实现,与 php-src 保持一致使用了 C 语言实现,目前正在准备 RFC 提案,贡献到 PHP 内核中
Swoole 与 Fiber 的差别
- Fiber 只是协程 Context 管理的一种实现,更像是 Generator 的升级版
- Swoole 是完整的协程 Runtime & Framework,更像是 Golang
Swoole 是否会使用 ext-fiber ?
暂时不会。有两方面的原因:1. Swoole 的实现是双层协程设计,底层是 C 协程,上层是 PHP 协程。而 Fiber 的实现耦合在一起的。Swoole 是内核协程化设计,在 core 层面对协程操作进行了封装,外层只需要调用 API 即可,不需要关心发生阻塞 IO 时协程如何切换,PHP 层实际上只是 wrapper ,2. Fiber 是以扩展方式加入 PHP 内核的,并不是 ZendAPI,地位等同于 curl/mysql 等扩展库。在 PHP 中扩展依赖管理做的很糟糕。处理不好容易出现找不到 符号(symbol not found),而 Swoole 同样也是 PHP 的一个扩展,它与 ext-fiber 是平级关系,协程是 Swoole 的核心部分,不太好依赖另外一个库的实现。
当然 Swoole 会对齐 ext-fiber 在 PHP 协程切换部分的代码,保证一致性。由于 PHP 暂时还未提供 EventLoop 的基础设施,Swoole 扩展提供的功能和其他基于 ext-fiber 扩展实现的 PHP 协程类库,不在同一个 Loop 中,也无法实现共存。
创建 C 协程
Coroutine::create([]() {
System::sleep(1.0);
});
创建 PHP 协程
zend_fcall_info_cache *func;
zval argv[2];
PHPCoroutine::create(func, 2, argv);
在 PHP 代码中创建协程
use function Swoole\Coroutine\run;
use function Swoole\Coroutine\go;
run(function () {
go(function () {
sleep(1);
});
});
我们也会持续关注 ext-fiber ,在未来某个节点,ext-fiber 足够成熟稳定,并且迁移到 ZendAPI 中时,Swoole 也会考虑使用 ext-fiber
文章评论