PHP 高性能的事件循環庫 Revolt
Revolt是什么?
Revolt是并發PHP應用程序的堅如磐石的事件循環。通常的PHP應用程序將大部分時間花在等待I/O上。雖然PHP是單線程的,但可以使用協作多任務來允許并發性,方法是使用等待時間來做不同的事情。
PHP的傳統同步執行流程很容易理解。一次只做一件事。如果查詢數據庫,則發送查詢并等待數據庫服務器的響應。一旦你有了答案,你就可以開始做下一件事。
ReactPHP和其他庫已經在PHP中提供了很長一段時間的協作多任務。然而,它們的事件驅動特性與許多現有的接口不兼容,需要不同的思維模型。PHP 8.1內置了fibers,它提供了協作多線程。調用可以是異步的,沒有promise或回調,同時仍然允許非阻塞I/O。
每個使用協同多任務的應用程序都需要一個調度器(也稱為事件循環),這個包提供了這個調度器。Revolt是結合了React和ReactPHP的事件循環實現的多年經驗的結果。然而,它并不是一個用于編寫并發PHP應用程序的成熟框架,而只是提供了必要的公共基礎。不同的(強烈的)固執己見的庫可以在它的基礎上構建,React和ReactPHP將繼續共存。
Revolt 支持事件
- Defer 回調在事件循環的下一次迭代中執行。如果有延遲調度,事件循環不會在迭代之間等待。
- Delay 在指定的秒數后執行回調。秒的分數可以表示為浮點數。
- Repeat 在指定的秒數后重復執行回調。秒的分數可以表示為浮點數。
- Stream readable 當流上有數據要讀取或連接關閉時,將執行回調。
- Stream writable 當寫緩沖區中有足夠的空間來接受要寫入的新數據時,就會執行回調。
- Signal 當進程從操作系統接收到特定信號時執行回調。
安裝
composer require revolt/event-loop
注意:此包可以作為Composer依賴項安裝在PHP 8.1及更高版本上。
示例
<?php
require __DIR__ . '/vendor/autoload.php';
use Revolt\EventLoop;
$suspension = EventLoop::getSuspension();
$repeatId = EventLoop::repeat(1, function (): void {
print '++ Executing callback created by EventLoop::repeat()' . PHP_EOL;
});
EventLoop::delay(5, function () use ($suspension, $repeatId): void {
print '++ Executing callback created by EventLoop::delay()' . PHP_EOL;
EventLoop::cancel($repeatId);
$suspension->resume(null);
print '++ Suspension::resume() is async!' . PHP_EOL;
});
print '++ Suspending to event loop...' . PHP_EOL;
$suspension->suspend();
print '++ Script end' . PHP_EOL;
在執行上面的例子時,你應該看到這樣的輸出:
++ Suspending to event loop...
++ Executing callback created by EventLoop::repeat()
++ Executing callback created by EventLoop::repeat()
++ Executing callback created by EventLoop::repeat()
++ Executing callback created by EventLoop::repeat()
++ Executing callback created by EventLoop::delay()
++ Suspension::resume() is async!
++ Script end
這個輸出說明了事件循環內部發生的事情就像它自己獨立的程序一樣。您的腳本將不會繼續通過 $suspension->suspend() 點,除非掛起點通過 $suspension->resume() 或 $suspension->throw() 恢復。
雖然一個應用程序可以而且經常幾乎完全在事件循環的范圍內發生,但我們也可以使用事件循環來做一些事情,比如下面的例子,它為交互式控制臺輸入施加了一個短暫的超時:
<?php
require __DIR__ . '/vendor/autoload.php';
use Revolt\EventLoop;
if (\stream_set_blocking(STDIN, false) !== true) {
\fwrite(STDERR, "Unable to set STDIN to non-blocking" . PHP_EOL);
exit(1);
}
print "Write something and hit enter" . PHP_EOL;
$suspension = EventLoop::getSuspension();
$readableId = EventLoop::onReadable(STDIN, function ($id, $stream) use ($suspension): void {
EventLoop::cancel($id);
$chunk = \fread($stream, 8192);
print "Read " . \strlen($chunk) . " bytes" . PHP_EOL;
$suspension->resume(null);
});
$timeoutId = EventLoop::delay(5, function () use ($readableId, $suspension) {
EventLoop::cancel($readableId);
print "Timeout reached" . PHP_EOL;
$suspension->resume(null);
});
$suspension->suspend();
EventLoop::cancel($readableId);
EventLoop::cancel($timeoutId);
顯然,我們可以在這個例子中簡單地同步使用 fgets(STDIN) 。我們只是在演示可以根據需要進出事件循環,以混合同步任務和非阻塞任務。
Timers 定時器
事件循環公開了幾種調度計時器的方法。
Deferred 回調
- defer() 調度回調在事件循環的下一次迭代中執行。
- 此方法保證了一個干凈的調用堆棧,以避免循環的當前迭代中其他事件的饑餓。defer()回調總是在事件循環的下一個tick中執行。
- 在 defer()計時器執行之后,它會被事件循環自動垃圾收集,因此應用程序不需要手動取消關聯的回調。
- 像所有事件回調一樣,defer() 計時器可以被禁用和重新啟用。如果您在調度它和它實際運行之間禁用此回調,則事件循環將無法對其進行垃圾收集,直到它執行為止。因此,如果 defer() 回調從未真正執行以釋放任何相關資源,則必須手動取消該回調。
案例
<?php
/**
* @author Tinywan(ShaoBo Wan)
* @email 756684177@qq.com
* @date 2024/1/31 18:24
*/
require 'vendor/autoload.php';
use Revolt\EventLoop;
echo "line 1\n";
EventLoop::defer(function (): void {
echo "line 3\n";
});
echo "line 2\n";
EventLoop::run();
輸出
line 1
line 2
line 3
Delayed 回調
- delay() 計劃在延遲 n 秒后執行回調
- delay() 回調在執行后也會被事件循環自動垃圾回收,應用程序不應該手動取消它,除非他們希望在執行前完全放棄回調。
- 被禁用的 delay() 回調會重置其延遲時間,以便重新啟用后,原始延遲時間再次從零開始。
- 與 defer() 回調一樣,如果定時器在創建后被應用程序禁用而無法運行,則必須手動取消計劃用于一次性執行的定時器以釋放資源。
案例
<?php
/**
* @author Tinywan(ShaoBo Wan)
* @email 756684177@qq.com
* @date 2024/1/31 18:24
*/
require 'vendor/autoload.php';
use Revolt\EventLoop;
EventLoop::delay(3, function (): void {
print '3 seconds passed';
});
EventLoop::run();
3秒后輸出
3 seconds passed
Periodic 定期回調
- repeat() 調度回調以每 n 秒重復執行一次。
- 與所有其他事件回調一樣, repeat() 定時器可以隨時禁用/重新啟用。
- 與 defer() 和 delay() 回調不同, repeat() 回調必須顯式取消以釋放關聯的資源。一旦 repeat() 回調的目的實現,如果不能通過 cancel() 釋放它們,將導致應用程序中的內存泄漏。僅僅禁用 repeat() 回調是不夠的,因為它們的數據只有在取消時才被釋放。
案例
<?php
/**
* @author Tinywan(ShaoBo Wan)
* @email 756684177@qq.com
* @date 2024/1/31 18:49
*/
require 'vendor/autoload.php';
use Revolt\EventLoop;
EventLoop::repeat(0.1, function ($callbackId): void {
static $i = 0;
if ($i++ < 3) {
echo "tick\n";
} else {
EventLoop::cancel($callbackId);
}
});
EventLoop::run();
輸出
tick
tick
tick
定時器偏差
重復計時器基本上是簡單的延遲計時器,在觸發適當的處理程序之前會自動重新調度。它們受定時器漂移的影響。多個計時器可能會堆疊在一起,以防它們作為協程執行。
Fibers 纖程
Revolt被設計為可以很好地與纖維一起工作。所有事件回調都在單獨的纖程中運行,并且可以隨時掛起它。如果在事件回調中沒有掛起,則纖程將被重用于將來的事件回調以保存資源。
掛起允許通過掛起當前執行上下文來等待事件,直到所討論的事件發生。它們將掛起當前纖程并返回到事件循環,或者如果從纖程外部(即從 {main} )調用,則開始運行事件循環。
應使用 Revolt\EventLoop\Suspension API暫停和恢復光纖。Suspension 對象可以使用 Revolt\EventLoop::getSuspension() 創建。在獲得 Suspension 對象之后,可以注冊事件回調以調度當前纖程的恢復。$suspension->suspend() 將掛起當前的執行上下文,直到它通過 $suspension->resume() 或 $suspension->throw()恢復。
案例:讓我們暫停主執行上下文,直到有數據從 STDIN 讀取或超時到期:
<?php
require __DIR__ . '/vendor/autoload.php';
use Revolt\EventLoop;
if (\stream_set_blocking(STDIN, false) !== true) {
\fwrite(STDERR, "Unable to set STDIN to non-blocking" . PHP_EOL);
exit(1);
}
print "Write something and hit enter" . PHP_EOL;
$suspension = EventLoop::getSuspension();
$readableId = EventLoop::onReadable(STDIN, function ($id, $stream) use ($suspension): void {
EventLoop::cancel($id);
$chunk = \fread($stream, 8192);
print "Read " . \strlen($chunk) . " bytes" . PHP_EOL;
$suspension->resume(null);
});
$timeoutId = EventLoop::delay(5, function () use ($readableId, $suspension) {
EventLoop::cancel($readableId);
print "Timeout reached" . PHP_EOL;
$suspension->resume(null);
});
$suspension->suspend();
EventLoop::cancel($readableId);
EventLoop::cancel($timeoutId);
自動超時輸出
Write something and hit enter
Timeout reached
按Enter鍵盤輸出
Write something and hit enter
Read 1 bytes
Signals 信號
信號是類Unix操作系統中的標準化消息。
EventLoop::onSignal() 可用于對發送到進程的信號作出反應。
<?php
require __DIR__ . '/vendor/autoload.php';
use Revolt\EventLoop;
// Let's tick off output once per second, so we can see activity.
EventLoop::repeat(1, function (): void {
echo "tick: ", date('c'), "\n";
});
// What to do when a SIGINT signal is received
EventLoop::onSignal(SIGINT, function (): void {
echo "Caught SIGINT! exiting ...\n";
exit;
});
EventLoop::run();
SIGINT 信號: 當用戶按某些終端鍵時, 引發終端產生的信號. 如Ctrl+C鍵, 這將產生中斷信號SIGINT. 它將停止一個已失去控制的程序。
Ctrl+C 輸出
tick: 2024-01-31T11:54:03+00:00
tick: 2024-01-31T11:54:04+00:00
tick: 2024-01-31T11:54:05+00:00
tick: 2024-01-31T11:54:06+00:00
tick: 2024-01-31T11:54:07+00:00
tick: 2024-01-31T11:54:08+00:00
tick: 2024-01-31T11:54:09+00:00
tick: 2024-01-31T11:54:10+00:00
tick: 2024-01-31T11:54:11+00:00
tick: 2024-01-31T11:54:12+00:00
^CCaught SIGINT! exiting ...
從基本原理中可以清楚地看到,信號回調可以像任何其他事件回調一樣被啟用、禁用和取消。一般來說,如果所有回調都消失了,只有信號回調仍然存在,那么您希望退出事件循環,除非您沒有主動等待該事件發生。
信號號可用性
ext-uv 暴露 UV::SIG* 常量用于可觀察信號。使用 EventDriver 的應用程序在注冊信號回調或依賴 ext-pcntl 時需要手動指定適當的整數信號編號。