m6web / tornado
异步编程库。
Requires
- php: ^7.3|^8.0
- psr/http-message: ^1.0
Requires (Dev)
- ext-curl: ^7.3|^8.0
- amphp/amp: ^2.0
- guzzlehttp/guzzle: ^6.3
- http-interop/http-factory-guzzle: ^1.0
- m6web/php-cs-fixer-config: ^2.0
- phpstan/phpstan: ^0.12
- phpunit/phpunit: ^9.4.4
- psr/http-factory: ^1.0
- react/event-loop: ^1.0
- react/promise: ^2.7
- symfony/http-client: ^4.3
Suggests
- ext-curl: Required to use Curl and HTTP2 features
- amphp/amp: Required to use Tornado\Adapter\Amp\EventLoop
- guzzlehttp/guzzle: Required to use Tornado\Adapter\Guzzle\HttpClient
- psr/http-factory: Required to use Tornado\Adapter\Symfony\HttpClient
- react/event-loop: Required to use Tornado\Adapter\ReactPhp\EventLoop
- react/promise: Required to use Tornado\Adapter\ReactPhp\EventLoop
- symfony/http-client: Required to use Tornado\Adapter\Symfony\HttpClient
This package is auto-updated.
Last update: 2024-08-24 11:02:57 UTC
README
一个用于 Php 的异步编程库。
Tornado 包含用于使用 生成器 编写异步程序的几个接口。此库为流行的异步框架(ReactPhp,Amp)提供适配器,并内置适配器以了解如何编写自己的。
安装
您可以使用 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
。当使用 yield
或 EventLoop::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 } })());
常见问题解答
Tornado 与 Tornado 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