react/event-loop

ReactPHP 的核心反应器事件循环,库可以使用它进行事件驱动的 I/O。

资助包维护!
Open Collective

安装次数: 62,601,090

依赖者: 655

建议者: 14

安全: 0

星标: 1,232

关注者: 49

分支: 127

开放问题: 4

v1.5.0 2023-11-13 13:48 UTC

README

CI status installs on Packagist

ReactPHP 的核心反应器事件循环,库可以使用它进行事件驱动的 I/O。

开发版本: 此分支包含即将推出的 v3 版本的代码。要查看当前稳定版 v1 的代码,请查看 1.x 分支

即将推出的 v3 版本将是此包的发展方向。然而,我们仍将积极支持 v1 版本,以帮助尚未升级到最新版本的用户。有关更多详细信息,请参阅 安装说明

为了使基于异步的库能够互操作,它们需要使用相同的事件循环。此组件提供了一个通用的 LoopInterface,任何库都可以针对它。这使得它们可以在同一个循环中一起使用,通过一个由用户控制的单个 run() 调用来使用。

目录

快速入门示例

这是一个仅使用事件循环构建的异步 HTTP 服务器。

<?php

use React\EventLoop\Loop;

require __DIR__ . '/vendor/autoload.php';

$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, false);

Loop::addReadStream($server, function ($server) {
    $conn = stream_socket_accept($server);
    $data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n";
    Loop::addWriteStream($conn, function ($conn) use (&$data) {
        $written = fwrite($conn, $data);
        if ($written === strlen($data)) {
            fclose($conn);
            Loop::removeWriteStream($conn);
        } else {
            $data = substr($data, $written);
        }
    });
});

Loop::addPeriodicTimer(5, function () {
    $memory = memory_get_usage() / 1024;
    $formatted = number_format($memory, 3).'K';
    echo "Current memory usage: {$formatted}\n";
});

另请参阅 示例

使用方法

典型应用程序会使用 Loop 来使用默认事件循环,如下所示

use React\EventLoop\Loop;

$timer = Loop::addPeriodicTimer(0.1, function () {
    echo 'Tick' . PHP_EOL;
});

Loop::addTimer(1.0, function () use ($timer) {
    Loop::cancelTimer($timer);
    echo 'Done' . PHP_EOL;
});

作为替代,您也可以在程序开始时显式创建事件循环实例,在整个程序中重用它,并在程序结束时运行它,如下所示

$loop = React\EventLoop\Loop::get();

$timer = $loop->addPeriodicTimer(0.1, function () {
    echo 'Tick' . PHP_EOL;
});

$loop->addTimer(1.0, function () use ($loop, $timer) {
    $loop->cancelTimer($timer);
    echo 'Done' . PHP_EOL;
});

$loop->run();

虽然前者更简洁,但后者更明确。在两种情况下,程序都会执行完全相同的步骤。

  1. 事件循环实例在程序开始时创建。这是在第一次调用 Loop(或通过手动实例化任何 循环实现)时隐式完成的。
  2. 事件循环直接使用或作为实例传递给库和应用程序代码。在此示例中,将周期性计时器注册到事件循环中,该计时器简单地每秒输出一次 Tick,直到另一个计时器在秒后停止周期性计时器。
  3. 在程序结束时运行事件循环。当使用 Loop 时,这会自动完成;或者在程序结束时使用单个 run() 调用来显式完成。

v1.2.0 版本起,我们强烈建议使用 Loop。显式循环指令仍然有效,在某些应用中可能仍然有用,尤其是在过渡到更简洁风格的期间。

循环

Loop 类作为事件循环的便捷全局访问器而存在。

循环方法

Loop 类提供了在 LoopInterface 上存在的所有方法,作为静态方法。

如果你正在你的应用程序代码中处理事件循环,直接与 Loop 类上定义的静态方法交互通常更容易,如下所示

use React\EventLoop\Loop;

$timer = Loop::addPeriodicTimer(0.1, function () {
    echo 'Tick' . PHP_EOL;
});

Loop::addTimer(1.0, function () use ($timer) {
    Loop::cancelTimer($timer);
    echo 'Done' . PHP_EOL;
});

另一方面,如果你熟悉面向对象编程(OOP)和依赖注入(DI),你可能想注入一个事件循环实例,并在 LoopInterface 上调用实例方法,如下所示

use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;

class Greeter
{
    private $loop;

    public function __construct(LoopInterface $loop)
    {
        $this->loop = $loop;
    }

    public function greet(string $name)
    {
        $this->loop->addTimer(1.0, function () use ($name) {
            echo 'Hello ' . $name . '!' . PHP_EOL;
        });
    }
}

$greeter = new Greeter(Loop::get());
$greeter->greet('Alice');
$greeter->greet('Bob');

每个静态方法调用将通过使用内部 Loop::get() 调用原样转发到底层事件循环实例。有关可用方法的更多详细信息,请参阅 LoopInterface

循环自动运行

当使用 Loop 类时,它将在程序末尾自动执行循环。这意味着以下示例将安排一个计时器,并会自动执行程序,直到计时器事件触发。

use React\EventLoop\Loop;

Loop::addTimer(1.0, function () {
    echo 'Hello' . PHP_EOL;
});

v1.2.0 版本起,我们强烈建议以这种方式使用 Loop 类并省略任何显式的 run() 调用。出于向后兼容的原因,显式的 run() 方法仍然有效,并且在某些应用中可能仍然有用,尤其是在过渡到更简洁风格的过程中。

如果你不希望 Loop 自动运行,你可以显式地调用 run()stop() 它。如果你正在使用全局异常处理程序,这可能会很有用,如下所示

use React\EventLoop\Loop;

Loop::addTimer(10.0, function () {
    echo 'Never happens';
});

set_exception_handler(function (Throwable $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
    Loop::stop();
});

throw new RuntimeException('Demo');

get()

get(): LoopInterface 方法可用于获取当前活动的事件循环实例。

此方法将始终在应用程序的生命周期内返回相同的事件循环实例。

use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;

$loop = Loop::get();

assert($loop instanceof LoopInterface);
assert($loop === Loop::get());

这对于使用面向对象编程(OOP)和依赖注入(DI)特别有用。在这种情况下,你可能想注入一个事件循环实例,并在 LoopInterface 上调用实例方法,如下所示

use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;

class Greeter
{
    private $loop;

    public function __construct(LoopInterface $loop)
    {
        $this->loop = $loop;
    }

    public function greet(string $name)
    {
        $this->loop->addTimer(1.0, function () use ($name) {
            echo 'Hello ' . $name . '!' . PHP_EOL;
        });
    }
}

$greeter = new Greeter(Loop::get());
$greeter->greet('Alice');
$greeter->greet('Bob');

有关可用方法的更多详细信息,请参阅 LoopInterface

循环实现

除了 LoopInterface 之外,还提供了一些事件循环实现。

所有事件循环都支持以下功能

  • 文件描述符轮询
  • 一次性定时器
  • 周期性定时器
  • 在未来的循环tick上延迟执行

对于大多数此包的消费者来说,底层事件循环实现是实现细节。你应该使用 Loop 来自动创建新实例。

高级!如果你需要特定的事件循环实现,你可以手动实例化以下类之一。请注意,你可能需要首先安装相应事件循环实现的所需PHP扩展,否则它们在创建时会抛出 BadMethodCallException

StreamSelectLoop

基于 stream_select() 的事件循环。

这使用 stream_select() 函数,并且是唯一一个与PHP开箱即用的实现。

该事件循环在所有PHP版本上都能直接运行。这意味着不需要安装任何东西,并且这个库可以在所有平台和受支持的PHP版本上运行。因此,如果您没有安装以下列出的任何事件循环扩展,Loop将默认使用此事件循环。

在底层,它执行一个简单的select系统调用。这个系统调用受限于FD_SETSIZE(平台相关,通常是1024)的最大文件描述符数,并且与O(m)m为传入的最大文件描述符数)成比例。这意味着在同时处理数千个流时可能会遇到问题,此时您可能需要考虑使用以下列出的替代事件循环实现之一。如果您的情况属于许多只处理数十个或几百个流的常见用例之一,那么这个事件循环实现的表现非常出色。

如果您想使用信号处理(请参阅下面的addSignal()),则需要ext-pcntl扩展。此扩展仅在类Unix平台上可用,不支持Windows。它通常作为许多PHP发行版的一部分进行安装。如果缺少此扩展(或在Windows上运行),则不支持信号处理,并会抛出BadMethodCallException

已知此事件循环在PHP 7.3之前的任何版本中都依赖于系统时间来安排未来的定时器,因为只有从PHP 7.3开始才有单调时间源(hrtime())。虽然这不会影响许多常见用例,但对于依赖于高时间精度或系统会受到不连续时间调整(时间跳跃)的程序来说,这是一个重要的区别。这意味着如果您在PHP < 7.3上安排一个30秒后触发的定时器,然后向前调整系统时间20秒,定时器可能在10秒后触发。有关更多详细信息,请参阅addTimer()

ExtEventLoop

基于ext-event的事件循环。

这使用event PECL扩展,它提供了一个到libevent库的接口。libevent本身支持多种系统特定的后端(epoll,kqueue)。

此循环已知与PHP 7.1到PHP 8+兼容。

ExtEvLoop

基于ext-ev的事件循环。

此循环使用ev PECL扩展,它提供了一个到libev库的接口。libev本身支持多种系统特定的后端(epoll,kqueue)。

此循环已知与PHP 7.1到PHP 8+兼容。

ExtUvLoop

基于ext-uv的事件循环。

此循环使用uv PECL扩展,它提供了一个到libuv库的接口。libuv本身支持多种系统特定的后端(epoll,kqueue)。

此循环已知与PHP 7.1到PHP 8+兼容。

LoopInterface

run()

可以使用run(): void方法运行事件循环,直到没有更多任务要执行。

对于许多应用程序,此方法是事件循环上唯一直接可见的调用。一般来说,通常建议将所有内容附加到同一个循环实例,然后在应用程序的底部运行一次循环。

$loop->run();

此方法将保持循环运行,直到没有更多任务要执行。换句话说:此方法将阻塞,直到最后一个定时器、流和/或信号被移除。

同样,确保应用程序实际调用此方法一次至关重要。向循环添加监听器而未实际运行它将导致应用程序在没有等待任何附加监听器的情况下退出。

此方法在循环已运行时不应该被调用。在显式调用过stop()方法或由于之前没有事情可做而自动停止后,此方法可以被多次调用。

stop()

stop(): void方法可用于指示正在运行的事件循环停止。

此方法被视为高级用法,应谨慎使用。一般来说,只有在循环不再有事情可做时才建议自动停止循环。

此方法可以显式指令事件循环停止。

$loop->addTimer(3.0, function () use ($loop) {
    $loop->stop();
});

在当前未运行或已经停止的循环实例上调用此方法没有任何效果。

addTimer()

addTimer(float $interval, callable $callback): TimerInterface方法可用于在给定时间间隔后执行一次回调。

第二个参数必须是一个接受计时器实例作为唯一参数的计时器回调函数。如果你在计时器回调函数中不使用计时器实例,你可以使用没有任何参数的函数。

计时器回调函数不得抛出Exception。计时器回调函数的返回值将被忽略,因此出于性能考虑,建议不要返回任何过多数据结构。

此方法返回一个计时器实例。与上述所述,相同的计时器实例也将传递给计时器回调函数。你可以调用cancelTimer来取消挂起的计时器。与addPeriodicTimer()不同,此方法将确保回调在给定时间间隔后只被调用一次。

$loop->addTimer(0.8, function () {
    echo 'world!' . PHP_EOL;
});

$loop->addTimer(0.3, function () {
    echo 'hello ';
});

请参阅示例 #1

如果你想在回调函数中访问任何变量,你可以像这样将任意数据绑定到回调闭包中

function hello($name, LoopInterface $loop)
{
    $loop->addTimer(1.0, function () use ($name) {
        echo "hello $name\n";
    });
}

hello('Tester', $loop);

此接口不强制执行任何特定的计时器分辨率,因此如果你依赖于毫秒级或更高精度,可能需要特别注意。事件循环实现应尽可能工作,并应提供至少毫秒级的精度,除非另有说明。许多现有的事件循环实现已知提供微秒级精度,但通常不建议依赖于这种高精度。

类似地,在相同时间(在可能的精度范围内)安排执行的计时器的执行顺序也不保证。

此接口建议事件循环实现如果可用,应使用单调时间源。鉴于单调时间源默认从PHP 7.3开始可用,事件循环实现可能回退到使用系统时间。虽然这不会影响许多常见用例,但对于依赖于高时间精度或系统可能受到不连续时间调整(时间跳跃)的程序来说,这是一个重要的区别。这意味着如果你安排一个计时器在30秒后触发,然后调整你的系统时间前进了20秒,计时器仍然应该在30秒后触发。有关详细信息,请参阅事件循环实现

addPeriodicTimer()

addPeriodicTimer(float $interval, callable $callback): TimerInterface方法可用于在给定时间间隔后重复调用回调。

第二个参数必须是一个接受计时器实例作为唯一参数的计时器回调函数。如果你在计时器回调函数中不使用计时器实例,你可以使用没有任何参数的函数。

计时器回调函数不得抛出Exception。计时器回调函数的返回值将被忽略,因此出于性能考虑,建议不要返回任何过多数据结构。

此方法返回一个计时器实例。与上述描述的计时器回调函数相同,相同的计时器实例也将被传递。与addTimer()不同,此方法将确保在给定的时间间隔后无限期地调用回调,或者直到您调用cancelTimer

$timer = $loop->addPeriodicTimer(0.1, function () {
    echo 'tick!' . PHP_EOL;
});

$loop->addTimer(1.0, function () use ($loop, $timer) {
    $loop->cancelTimer($timer);
    echo 'Done' . PHP_EOL;
});

另请参阅示例 #2

如果您想限制执行次数,可以将任意数据绑定到回调闭包中,如下所示

function hello($name, LoopInterface $loop)
{
    $n = 3;
    $loop->addPeriodicTimer(1.0, function ($timer) use ($name, $loop, &$n) {
        if ($n > 0) {
            --$n;
            echo "hello $name\n";
        } else {
            $loop->cancelTimer($timer);
        }
    });
}

hello('Tester', $loop);

此接口不强制执行任何特定的计时器分辨率,因此如果你依赖于毫秒级或更高精度,可能需要特别注意。事件循环实现应尽可能工作,并应提供至少毫秒级的精度,除非另有说明。许多现有的事件循环实现已知提供微秒级精度,但通常不建议依赖于这种高精度。

类似地,在相同时间(在可能的精度范围内)安排执行的计时器的执行顺序也不保证。

此接口建议事件循环实现如果可用,应使用单调时间源。鉴于单调时间源默认从PHP 7.3开始可用,事件循环实现可能回退到使用系统时间。虽然这不会影响许多常见用例,但对于依赖于高时间精度或系统可能受到不连续时间调整(时间跳跃)的程序来说,这是一个重要的区别。这意味着如果你安排一个计时器在30秒后触发,然后调整你的系统时间前进了20秒,计时器仍然应该在30秒后触发。有关详细信息,请参阅事件循环实现

此外,由于每次调用后的重新调度,周期性计时器可能会受到计时器漂移的影响。因此,通常不建议依赖于它进行具有毫秒级精度或更低的高精度间隔。

cancelTimer()

cancelTimer(TimerInterface $timer): void方法可以用来取消挂起的计时器。

另请参阅addPeriodicTimer()示例 #2

如果调用此方法时计时器实例尚未添加到此循环实例,或者计时器已经被取消,则没有任何效果。

futureTick()

futureTick(callable $listener): void方法可以用来在事件循环的后续tick上调度回调。

这与具有零秒间隔的计时器非常相似,但不需要调度计时器队列的开销。

tick回调函数必须能够接受零个参数。

tick回调函数不得抛出Exception。tick回调函数的返回值将被忽略,没有效果,因此出于性能考虑,建议不要返回任何大量数据结构。

如果你想在回调函数中访问任何变量,你可以像这样将任意数据绑定到回调闭包中

function hello($name, LoopInterface $loop)
{
    $loop->futureTick(function () use ($name) {
        echo "hello $name\n";
    });
}

hello('Tester', $loop);

与计时器不同,tick回调保证按入队顺序执行。此外,一旦回调入队,就无法取消此操作。

这通常用于将大任务分解成更小的步骤(一种协作多任务的形式)。

$loop->futureTick(function () {
    echo 'b';
});
$loop->futureTick(function () {
    echo 'c';
});
echo 'a';

另请参阅示例 #3

addSignal()

addSignal(int $signal, callable $listener): void方法可以用来注册当此进程捕获到信号时要通知的监听器。

这对于捕获用户中断信号或来自像supervisorsystemd等工具的关闭信号很有用。

第二个参数必须是一个监听器回调函数,它接受信号作为其唯一的参数。如果您在监听器回调函数内不使用信号,可以使用没有任何参数的函数。

监听器回调函数不得抛出Exception。监听器回调函数的返回值将被忽略,没有效果,因此出于性能考虑,建议不要返回任何大量数据结构。

$loop->addSignal(SIGINT, function (int $signal) {
    echo 'Caught user interrupt signal' . PHP_EOL;
});

另请参阅示例 #4

信号仅在Unix-like平台上可用,由于操作系统限制,Windows不受支持。如果在此平台上不支持信号,则此方法可能会抛出BadMethodCallException,例如,当缺少所需扩展时。

注意:监听器只能添加一次到同一信号,任何尝试多次添加都会被忽略。

removeSignal()

removeSignal(int $signal, callable $listener): void方法可以用来移除之前添加的信号监听器。

$loop->removeSignal(SIGINT, $listener);

尝试移除未注册的监听器将被忽略。

addReadStream()

高级!请注意,这个低级API被认为是高级用法。大多数用例可能应该使用更高级的可读流API

可以使用addReadStream(resource $stream, callable $callback): void方法来注册一个监听器,当流准备好读取时被通知。

第一个参数必须是支持通过此循环实现检查其是否准备好读取的有效流资源。单个流资源不得添加超过一次。相反,要么先调用removeReadStream(),要么使用单个监听器对此事件做出反应,然后从该监听器进行分发。如果给定的资源类型不支持此循环实现,则此方法可能会抛出Exception

第二个参数必须是接受流资源作为其唯一参数的监听器回调函数。如果您在监听器回调函数中不使用流资源,可以使用没有任何参数的函数。

监听器回调函数不得抛出Exception。监听器回调函数的返回值将被忽略,没有效果,因此出于性能考虑,建议不要返回任何大量数据结构。

如果你想在回调函数中访问任何变量,你可以像这样将任意数据绑定到回调闭包中

$loop->addReadStream($stream, function ($stream) use ($name) {
    echo $name . ' said: ' . fread($stream);
});

另请参阅示例#11

您可以使用removeReadStream()来移除此流的读取事件监听器。

当多个流同时准备好时,监听器的执行顺序没有保证。

已知某些事件循环实现仅在流变为可读时才会触发监听器(边沿触发),并且可能不会在流从一开始就可用时触发。这也意味着,当PHP内部流缓冲区中仍有数据时,流可能不会被识别为可读。因此,建议在这种情况下使用stream_set_read_buffer($stream, 0);来禁用PHP的内部读取缓冲区。

addWriteStream()

高级!请注意,这个低级API被认为是高级用法。大多数用例可能应该使用更高级的可写流API

可以使用addWriteStream(resource $stream, callable $callback): void方法来注册一个监听器,当流准备好写入时被通知。

第一个参数必须是支持通过此循环实现检查其是否准备好写入的有效流资源。单个流资源不得添加超过一次。相反,要么先调用removeWriteStream(),要么使用单个监听器对此事件做出反应,然后从该监听器进行分发。如果给定的资源类型不支持此循环实现,则此方法可能会抛出Exception

第二个参数必须是接受流资源作为其唯一参数的监听器回调函数。如果您在监听器回调函数中不使用流资源,可以使用没有任何参数的函数。

监听器回调函数不得抛出Exception。监听器回调函数的返回值将被忽略,没有效果,因此出于性能考虑,建议不要返回任何大量数据结构。

如果你想在回调函数中访问任何变量,你可以像这样将任意数据绑定到回调闭包中

$loop->addWriteStream($stream, function ($stream) use ($name) {
    fwrite($stream, 'Hello ' . $name);
});

另请参阅示例#12

您可以使用removeWriteStream()来移除此流的写入事件监听器。

当多个流同时准备好时,监听器的执行顺序没有保证。

removeReadStream()

可以使用removeReadStream(resource $stream): void方法来移除给定流的读取事件监听器。

从已经从循环中移除的流中移除流或尝试移除从未添加或无效的流没有任何效果。

removeWriteStream()

可以使用removeWriteStream(resource $stream): void方法来移除给定流的写入事件监听器。

从已经从循环中移除的流中移除流或尝试移除从未添加或无效的流没有任何效果。

安装

安装此库的推荐方式是通过ComposerComposer新手?

该项目发布后,将遵循SemVer规范。目前,它将安装最新版本的开发版本

composer require react/event-loop:^3@dev

有关版本升级的详细信息,请参阅变更日志

该项目旨在在任何平台上运行,因此不需要任何PHP扩展,并支持在PHP 7.1到当前PHP 8+上运行。强烈建议为此项目使用最新支持的PHP版本。

建议安装任何事件循环扩展,但这完全可选。有关更多详细信息,请参阅事件循环实现

测试

要运行测试套件,您首先需要克隆此存储库,然后通过Composer安装所有依赖项。

composer install

要运行测试套件,请转到项目根目录并运行

vendor/bin/phpunit

许可证

MIT协议,请参阅许可文件

更多

  • 有关在现实世界应用程序中使用流的信息,请参阅我们的流组件
  • 请参阅我们的用户维基Packagist上的依赖项,以获取使用EventLoop的现实世界应用程序中使用的包列表。