PHP 高性能的事件循环库 Revolt

开发 前端
ext-uv​ 暴露 UV::SIG*​ 常量用于可观察信号。使用 EventDriver​ 的应用程序在注册信号回调或依赖 ext-pcntl 时需要手动指定适当的整数信号编号。

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 时需要手动指定适当的整数信号编号。


责任编辑:武晓燕 来源: 开源技术小栈
相关推荐

2014-11-25 10:03:42

JavaScript

2014-02-18 10:22:04

Objective-C循环

2019-03-14 15:38:19

ReactJavascript前端

2023-12-14 08:01:08

事件管理器Go

2021-05-27 10:02:57

Go缓存数据

2015-07-23 15:17:37

JavaScript循环语句

2024-11-06 11:25:06

2015-03-27 11:42:44

日志管理PHPSeasLog

2022-06-29 08:55:46

orjsonPythonJSON

2021-05-28 05:18:08

PHP语言roadrunnner

2024-10-21 09:06:15

2012-08-08 10:10:31

PHP

2011-01-26 10:46:30

FreeBSD 8.0NginxPHP

2018-10-10 14:27:34

数据库连接池MySQL

2019-07-23 11:41:45

数据库SQLDocker

2015-03-13 19:34:41

2023-09-22 11:48:37

2024-08-12 08:43:09

2018-06-01 14:00:00

数据库MySQL分库分表

2023-09-18 09:10:11

Golang高性能缓存库
点赞
收藏

51CTO技术栈公众号