在PHP中管理异步:从承诺到协程


什么是异步? 简而言之,异步意味着在特定时间段内执行多个任务。 PHP在单个线程中运行,这意味着在任何给定时间只能执行一段PHP代码。 这似乎是一种限制,但实际上给了我们更多的自由。 因此,我们不必面对与多线程编程相关的所有复杂性。 但是另一方面,存在一系列问题。 我们必须处理异步问题。 我们需要以某种方式对其进行管理和协调。


介绍来自Skyeng后端开发人员Sergey Zhuk博客的文章翻译。


例如,当我们执行两个并行的HTTP请求时,我们说它们正在“并行运行”。 这通常很容易实现,但是当我们需要简化这些请求的响应时,例如当一个请求需要从另一个请求接收数据时,就会出现问题。 因此,最大的困难在于异步管理。 有几种不同的方法可以解决此问题。


当前不存在对用于管理PHP中的异步的高级抽象的内置支持,并且我们必须使用第三方库,例如ReactPHP和Amp。 在本文的示例中,我使用ReactPHP。

承诺


为了更好地理解诺言的概念,将提供一个现实的例子。 想象您在麦当劳,想下订单。 您为此付款,从而开始交易。 为响应此交易,您希望获得一个汉堡包和炸薯条。 但是收银员不会立即退还食物。 相反,您会收到带有订单号的支票。 将此检查视为对未来订单的承诺。 现在,您可以检查一下并开始考虑您的美味午餐。 预期的汉堡包和薯条尚未准备好,因此您站起来等待,直到您的订单完成。 屏幕上出现他的电话号码后,您将立即将支票换成您的订单。 这些是承诺:


替代未来价值。

一个promise是对未来含义的一种表示,它是一个围绕时间的独立于时间的包装器。 我们不在乎该值是否已经存在。 我们继续以同样的方式来思考他。 想象一下,我们有三个异步HTTP请求,它们是“并行”执行的,因此它们将在大约一个时间点完成。 但是我们希望以某种方式协调和组织他们的答案。 例如,我们希望在收到答案后立即打印出来,但有一点限制:在收到第一个答案之前,不要打印第二个答案。 在这里,我的意思是,如果$ $ promise1被满足,那么我们将其打印出来。 但是,如果$ promise2首先被满足,我们将不打印它,因为$ promise1仍在进行中。 想象一下,我们正在尝试对三种竞争性查询进行调整,以使对于最终用户而言,它们看起来像一个快速请求。


那么,我们如何用承诺解决这个问题呢? 首先,我们需要一个返回承诺的函数。 我们可以收集三个这样的承诺,然后将它们放在一起。 这是一些伪造的代码:


<?php use React\Promise\Promise; function fakeResponse(string $url, callable $callback) { $callback("response for $url"); } function makeRequest(string $url) { return new Promise(function(callable $resolve) use ($url) { fakeResponse($url, $resolve); }); } 

在这里,我有两个功能:
fakeResponse(字符串$ url,可调用$回调)包含一个硬编码的响应,并允许指定的回调带有此答案;
makeRequest(字符串$ url)返回一个使用fakeResponse()表示请求已完成的承诺。


从客户端代码中,我们只需调用makeRequest()函数并获得promises:


 <?php $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3'); 

这很简单,但是现在我们需要以某种方式对这些答案进行排序。 再次,我们希望仅在第一个承诺完成后才打印第二个承诺的响应。 要解决此问题,您可以构建一个承诺链:


 <?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; }); 

在上面的代码中,我们从$ promise1开始。 完成后,我们将打印其值。 我们不在乎需要多长时间:不到一秒或一个小时。 承诺一经完成,我们便会打印其价值。 然后我们等待$ promise2 。 在这里,我们可以有两种情况:


$ promise2已经完成,我们立即打印它的值;
$ promise2仍在履行中,我们正在等待。


多亏了许诺的链接,我们不再需要担心某些许诺是否已经兑现。 Promis不依赖时间,因此它向我们隐藏了其状态(在此过程中,已经完成或被取消)。


这就是您可以通过诺言控制异步的方式。 而且看起来很棒,promise链比一堆嵌套的回调更漂亮,也更容易理解。


发电机


在PHP中,生成器是对可以暂停然后继续的功能的内置语言支持。 当此类生成器中的代码执行停止时,它看起来像是一个小的阻塞程序。 但是在此程序之外,在生成器之外,其他所有内容仍将继续工作。 这就是发电机的全部魔力。


我们实际上可以在本地暂停生成器,以等待诺言完成。 基本思想是将promise和生成器一起使用。 它们接管了异步的控制,当我们需要暂停生成器时,我们只调用yield。 这是相同的程序,但是现在我们正在连接生成器和Promise:


 <?php use Recoil\React\ReactKernel; // ... ReactKernel::start(function () { $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3'); var_dump(yield $promise1); var_dump(yield $promise2); var_dump(yield $promise3); }); 

对于此代码,我使用库recoilphp / recoil ,该库允许您调用ReactKernel :: start() Recoil使使用PHP生成器执行ReactPHP异步诺言成为可能。

在这里,我们仍然并行进行三个查询,但是现在我们使用yield关键字对响应进行排序。 再一次,我们在每个承诺的末尾显示结果,但仅在前一个承诺之后显示。


协程


协程是一种将操作或过程分为多个块的方法,每个此类块中都有一些执行。 结果,事实证明,不是一次执行整个操作(这可能导致应用程序明显冻结),而是逐步执行,直到完成所有必要的工作量。


现在我们有了可中断和可更新的生成器,我们可以使用它们以更熟悉的同步形式编写带有承诺的异步代码。 使用PHP生成器和Promise,您可以完全摆脱回调。 这个想法是,当我们给出一个承诺(使用yield调用)时,一个协程会订阅它。 Corutin暂停并等待,直到诺言完成(完成或取消)。 一旦诺言完成,协程将继续履行。 成功完成后,协程承诺将使用Generator :: send($ value)调用将接收到的值发送回生成器上下文。 如果promise失败,那么Corutin将使用Generator :: throw()调用通过生成器引发异常。 在没有回调的情况下,我们可以编写看起来几乎与通常的同步代码相似的异步代码。


顺序执行


使用协程时,异步代码中的执行顺序现在很重要。 该代码将精确地执行到调用yield关键字的位置,然后暂停直到promise完成。 考虑以下代码:


 <?php use Recoil\React\ReactKernel; // ... ReactKernel::start(function () { echo 'Response 1: ', yield makeRequest('url1'), PHP_EOL; echo 'Response 2: ', yield makeRequest('url2'), PHP_EOL; echo 'Response 3: ', yield makeRequest('url3'), PHP_EOL; }); 

Promise1:将显示在此处 ,然后暂停执行并等待。 一旦来自makeRequest('url1')的承诺完成后,我们将打印其结果并移至下一行代码。


错误处理


Promise / A + Promise标准指出每个Promise都包含then()和catch()方法。 该接口使您可以根据承诺构建链,还可以选择捕获错误。 考虑以下代码:


 <?php operation()->then(function ($result) { return anotherOperation($result); })->then(function ($result) { return yetAnotherOperation($result); })->then(function ($result) { echo $result; }); 

在这里,我们有一个承诺链,可以将每个先前的承诺的结果传递到下一个。 但是在此链中没有catch()块,这里没有错误处理。 当链中的承诺失败时,代码执行将移至链中最近的错误处理程序。 在我们的案例中,这意味着未履行的承诺将被忽略,抛出的任何错误将永远消失。 有了协程,错误处理就变得很重要。 如果任何异步操作失败,将引发异常:


 <?php use Recoil\React\ReactKernel; use React\Promise\RejectedPromise; // ... function failedOperation() { return new RejectedPromise(new RuntimeException('Something went wrong')); } ReactKernel::start(function () { try { yield failedOperation(); } catch (Throwable $error) { echo $error->getMessage() . PHP_EOL; } }); 

使异步代码可读


生成器有一个非常重要的副作用,我们可以用来控制异步,它解决了异步代码的可读性问题。 由于执行线程不断在程序的不同部分之间切换,因此我们很难理解异步代码将如何执行。 但是,我们的大脑基本上是同步运行的并且是单线程的。 例如,我们非常一致地计划一天:做一个,然后做另一件事,依此类推。 但是异步代码无法像我们的大脑习惯那样工作。 即使是简单的承诺链,也可能看起来不太可读:


 <?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; }); 

我们必须从精神上分解它,以便了解那里正在发生的事情。 因此,我们需要一个不同的模式来控制异步。 简而言之,生成器提供了一种编写异步代码的方式,使它看起来像是同步的。


Promise和Generators结合了两个方面的优点:我们获得了性能卓越的异步代码,但同时看起来却像是同步,线性和顺序的。 协程允许您隐藏异步,这已经成为实现细节。 同时我们的代码看起来就像我们的大脑习惯于线性地和顺序地思考。


如果我们在谈论ReactPHP ,那么您可以使用RecoilPHP库以协程的形式编写promise。 在Amp中,协程可以直接使用。

Source: https://habr.com/ru/post/zh-CN453296/


All Articles