PHP 8.1 的 Fiber RFC 大佬眼中的PHP Fiber提案

最新的 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);
});
});

 

轉載自 https://zhuanlan.zhihu.com/p/356942841

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章