m6web/tornado

异步编程库。

v0.4.0 2021-12-06 08:43 UTC

README

Tornado Logo

Build Status

一个用于 Php 的异步编程库。

Tornado 包含用于使用 生成器 编写异步程序的几个接口。此库为流行的异步框架(ReactPhpAmp)提供适配器,并内置适配器以了解如何编写自己的。

安装

您可以使用 Composer 安装它

composer require m6web/tornado

您还需要安装与您选择的 EventLoop 适配器相关的附加依赖项。您可以检查我们的建议,使用 Composer

composer suggests --by-package

ℹ️ Tornado 包含自己的 EventLoop 适配器以简化快速测试,并展示如何编写针对您的使用情况优化的 EventLoop,但请记住⚠️Tornado 适配器尚未准备好用于生产⚠️。

如何使用它

您可以在 examples 目录中找到现成的示例,但这里有一些关于异步编程和 Tornado 原则的详细说明。

处理承诺

EventLoop 负责执行所有异步函数。如果这些函数中的任何一个正在 等待 异步结果(一个 Promise),则 EventLoop 能够暂停此函数,并恢复其他可执行的函数。

当您获得一个 Promise 时,检索其具体值的唯一方法是 yield 它,让 EventLoop 内部处理 Php 生成器

/**
 * Sends a HTTP request a returns its body as a Json array.
 */
function getJsonResponseAsync(Tornado\HttpClient $httpClient, RequestInterface $request): \Generator
{
    /** @var ResponseInterface $response */
    $response = yield $httpClient->sendRequest($request);

    return json_decode((string) $response->getBody(), true);
}

⚠️ 请记住,返回类型在这里 不能array,即使我们期望 json_decode 返回一个 array。因为我们正在创建一个 Generator,所以返回类型定义为 \Generator

异步函数

一旦您的函数需要等待一个 Promise,按照定义,它就变成了一个异步函数。要执行它,您需要使用 EventLoop::async 方法。返回的 Promise 将由您的函数返回的值解决。

/**
 * Returns a Promise that will be resolved with a Json array.
 */
function requestJsonContent(Tornado\EventLoop $eventLoop, Tornado\HttpClient $httpClient): Tornado\Promise
{
    $request = new Psr7\Request(
        'GET',
        'http://httpbin.org/json',
        ['accept' => 'application/json']
    );

    return $eventLoop->async(getJsonResponseAsync($httpClient, $request));
}

⚠️ 请注意,公开一个 Generator 是一种不好的做法。您的异步函数应该返回一个 Promise,并保持其 Generator 作为实现细节。您可以选择以其他方式返回一个 Promise(参见 专用示例)。

运行事件循环

现在,您知道您需要创建一个生成器来等待一个 Promise,然后调用 EventLoop::async 来执行生成器并获得一个新的 Promise……但是,我们如何等待第一个 Promise 呢?实际上,还有另一种等待 Promise 的方法,一种 同步 的方法:使用 EventLoop::wait 方法。这意味着您应该 只使用一次,以同步方式等待预定义目标的解决。在内部,此函数将运行循环以处理所有 事件,直到达到目标(或发生错误,请参阅 专用章节)。

function waitResponseSynchronously(Tornado\EventLoop $eventLoop, Tornado\HttpClient $httpClient)
{
    /** @var array $jsonArray */
    $jsonArray = $eventLoop->wait(requestJsonContent($eventLoop, $httpClient));
    echo '>>> '.json_encode($jsonArray).PHP_EOL;
}

yield 关键字一样,EventLoop::wait 方法将返回输入 Promise 的解决值,但请记住,您应该仅在执行期间使用它一次。

并发

为了揭示异步编程的真正力量,我们必须在我们的程序中引入 并发。如果我们的目标是只发送一个 HTTP 请求并等待它,处理异步请求就没有任何好处。然而,一旦您有两个或更多目标要实现,异步函数将通过并发提高您的性能。为了解决多个独立的 Promises,请使用 EventLoop::promiseAll 方法创建一个新的 Promise,该 Promise 将在所有其他 Promise 都解决时解决。

function waitManyResponsesSynchronously(Tornado\EventLoop $eventLoop, Tornado\HttpClient $httpClient)
{
    $allJsonArrays = $eventLoop->wait(
        $eventLoop->promiseAll(
            requestJsonContent($eventLoop, $httpClient),
            requestJsonContent($eventLoop, $httpClient),
            requestJsonContent($eventLoop, $httpClient),
            requestJsonContent($eventLoop, $httpClient)
        )
    );

    foreach ($allJsonArrays as $index => $jsonArray) {
        echo "[$index]>>> ".json_encode($jsonArray).PHP_EOL;
    }
}

需要注意的是,由于并发的原因,使用 EventLoop::promiseAll 而不是逐个等待每个 Promise 的效率更高。每次你有多个待解决的承诺时,问问自己是否可以并发等待它们,尤其是在处理循环时(参考 EventLoop::promiseForeach 函数和对应示例)。

解决自己的承诺

按照设计,您不能自行解决承诺,您需要使用 Deferred。它允许您创建一个 Promise,并在不暴露这些高级控制的情况下解决(或拒绝)它。

function promiseWaiter(Tornado\Promise $promise): \Generator
{
    echo "I'm waiting a promise…\n";
    $result = yield $promise;
    echo "I received [$result]!\n";
}

function deferredResolver(Tornado\EventLoop $eventLoop, Tornado\Deferred $deferred): \Generator
{
    yield $eventLoop->delay(1000);
    $deferred->resolve('Hello World!');
}

function waitDeferredSynchronously(Tornado\EventLoop $eventLoop)
{
    $deferred = $eventLoop->deferred();
    $eventLoop->wait($eventLoop->promiseAll(
        $eventLoop->async(deferredResolver($eventLoop, $deferred)),
        $eventLoop->async(promiseWaiter($deferred->getPromise()))
    ));
}

错误管理

在成功的情况下,Promise 将被 解决,但在出现错误的情况下,它将被 拒绝 并带有 Throwable。当使用 yieldEventLoop::wait 等待 Promise 时可能会抛出异常,您可以选择捕获它或将它传播到上级。如果在异步函数中抛出异常,这将拒绝相关的 Promise

function failingAsynchronousFunction(Tornado\EventLoop $eventLoop): \Generator
{
    yield $eventLoop->idle();

    throw new \Exception('This is an exception!');
}

function waitException(Tornado\EventLoop $eventLoop)
{
    try {
        $eventLoop->wait($eventLoop->async(failingAsynchronousFunction($eventLoop)));
    } catch (\Throwable $throwable) {
        echo $throwable->getMessage().PHP_EOL;
    }
}

当使用 EventLoop::async 时,生成器内部抛出的所有异常都将拒绝返回的 Promise。在后台计算的情况下,您可以忽略此 Promise 而不使用 yield 或等待它,但 Tornado 仍然会捕获抛出的异常以防止错过它们。按照设计,被忽略的拒绝 Promise 将在销毁时抛出其异常。这意味着如果您真的想忽略所有异常(真的吗?),您必须在代码中明确地捕获和忽略它们。

$ignoredPromise = $eventLoop->async((function() {
  try {
    yield from throwingGenerator();
  } catch(\Throwable $throwable) {
      // I want to ignore all exceptions for this function
  }
})());

常见问题解答

TornadoTornado Python 库 有关系吗?

没有,尽管这两个库都处理异步编程,但它们绝对没有关系。选择 Tornado 这个名字是为了纪念 Zorro 骑的马

我喜欢你的标志,是谁设计的?

Tornado 标志是由 Cécile Moret 设计的。

贡献

运行单元测试

composer tests-unit

运行示例

composer tests-examples

运行 PhpStan(静态分析)

composer static-analysis

检查代码风格

composer code-style-check

修复代码风格

composer code-style-fix