PHP8.1的FiberRFC开发者眼中的PHPFiber提案
最新的 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 ⼀样的串⾏模式。协程实现中必须要考虑到如何解决这个问题。
php支持多线程吗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("www.baidu/");
});
});
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);
});
});

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。