Gerenciando a assincronia no PHP: das promessas às corotinas


O que é assincronia? Em suma, assincronia significa executar várias tarefas durante um período específico de tempo. O PHP é executado em um único encadeamento, o que significa que apenas uma parte do código PHP pode ser executada a qualquer momento. Isso pode parecer uma limitação, mas na verdade nos dá mais liberdade. Como resultado, não precisamos enfrentar toda a complexidade associada à programação multithread. Mas, por outro lado, há um conjunto de problemas. Temos que lidar com assincronia. Precisamos, de alguma forma, gerenciá-lo e coordená-lo.


Apresentando a tradução de um artigo do blog do desenvolvedor de back-end Skyeng, Sergey Zhuk.


Por exemplo, quando executamos duas solicitações HTTP paralelas, dizemos que elas estão "executando em paralelo". Isso geralmente é fácil e simples de fazer, mas surgem problemas quando precisamos simplificar as respostas dessas solicitações, por exemplo, quando uma solicitação requer dados recebidos de outra solicitação. Assim, é na administração da assincronia que está a maior dificuldade. Existem várias maneiras diferentes de resolver esse problema.


Atualmente, o PHP não possui suporte nativo para abstrações de alto nível para controlar a assincronia, e precisamos usar bibliotecas de terceiros, como ReactPHP e Amp. Nos exemplos deste artigo, eu uso o ReactPHP.

Promessas


Para entender melhor a idéia de promessas, um exemplo da vida real será útil. Imagine que você está no McDonald's e deseja fazer um pedido. Você paga por isso e, assim, inicia a transação. Em resposta a essa transação, você espera obter um hambúrguer e batatas fritas. Mas o caixa não devolve imediatamente a comida. Em vez disso, você recebe um cheque com o número do pedido. Considere esta verificação como uma promessa para um pedido futuro. Agora você pode fazer essa verificação e começar a pensar no seu delicioso almoço. O hambúrguer e as batatas fritas esperados ainda não estão prontos, então espere até que seu pedido seja concluído. Assim que o número dele aparecer na tela, você trocará o cheque pelo seu pedido. Estas são as promessas:


Substitua pelo valor futuro.

Uma promessa é uma representação para um significado futuro, um invólucro independente do tempo que envolvemos em torno de um significado. Não nos importamos se o valor já está aqui ou ainda não. Continuamos a pensar nele da mesma maneira. Imagine que temos três solicitações HTTP assíncronas que são executadas "em paralelo", para que sejam concluídas aproximadamente em um ponto no tempo. Mas queremos, de alguma forma, coordenar e organizar suas respostas. Por exemplo, queremos imprimir essas respostas assim que recebidas, mas com uma pequena restrição: não imprima a segunda resposta até que a primeira seja recebida. Aqui, quero dizer que, se $ promessa1 for cumprida, nós a imprimiremos. Porém, se $ promessa2 for cumprida primeiro, não a imprimiremos, porque ainda está em andamento. Imagine que estamos tentando adaptar três solicitações competitivas de forma que, para o usuário final, elas pareçam uma solicitação rápida.


Então, como podemos resolver esse problema com promessas? Primeiro de tudo, precisamos de uma função que retorne uma promessa. Podemos coletar três dessas promessas e depois colocá-las juntas. Aqui está um código falso para isso:


<?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); }); } 

Aqui eu tenho duas funções:
fakeResponse (string $ url, callable $ callback) contém uma resposta codificada e permite o retorno especificado com esta resposta;
makeRequest (string $ url) retorna uma promessa que usa fakeResponse () para indicar que a solicitação foi concluída.


No código do cliente, simplesmente chamamos a função makeRequest () e obtemos as promessas:


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

Era simples, mas agora precisamos ordenar essas respostas de alguma forma. Mais uma vez, queremos que a resposta da segunda promessa seja impressa somente após a conclusão da primeira. Para resolver esse problema, você pode criar uma cadeia de promessas:


 <?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'; }); 

No código acima, começamos com $ promessa1 . Depois de concluído, imprimimos seu valor. Não nos importamos quanto tempo leva: menos de um segundo ou uma hora. Assim que a promessa for concluída, imprimiremos seu valor. E então esperamos por $ promessa2 . E aqui podemos ter dois cenários:


$ promessa2está completo e imediatamente imprimimos seu valor;
O $ promessa2 ainda está sendo cumprido e estamos aguardando.


Graças ao encadeamento de promessas, não precisamos mais nos preocupar se alguma promessa foi ou não cumprida. Promis não depende de tempo e, portanto, oculta seus estados (no processo, já concluídos ou cancelados).


É assim que você pode controlar a assincronia com promessas. E parece ótimo, a cadeia de promessas é muito mais bonita e compreensível do que um monte de retornos aninhados.


Geradores


No PHP, os geradores são suporte de linguagem interna para funções que podem ser pausadas e, em seguida, continuadas. Quando a execução do código dentro desse gerador é interrompida, parece um pequeno programa bloqueado. Mas fora deste programa, fora do gerador, todo o resto continua funcionando. Isso é toda a magia e poder dos geradores.


Podemos literalmente pausar o gerador localmente para esperar a promessa ser concluída. A idéia básica é usar promessas e geradores juntos. Eles assumem o controle da assincronia e chamamos de rendimento quando precisamos suspender o gerador. Aqui está o mesmo programa, mas agora estamos conectando geradores e promessas:


 <?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); }); 

Para esse código, eu uso a biblioteca recoilphp / recoil , que permite chamar ReactKernel :: start () . Recoil torna possível usar geradores PHP para executar promessas assíncronas do ReactPHP.

Aqui, ainda estamos fazendo três consultas em paralelo, mas agora estamos classificando as respostas usando a palavra-chave yield . E, novamente, exibimos os resultados no final de cada promessa, mas somente após a anterior.


Coroutines


As corotinas são uma maneira de dividir uma operação ou processo em partes, com alguma execução dentro de cada uma dessas partes. Como resultado, verifica-se que, em vez de executar toda a operação por vez (o que pode levar a um congelamento perceptível do aplicativo), ela será executada gradualmente até que toda a quantidade necessária de trabalho seja concluída.


Agora que temos geradores interruptíveis e renováveis, podemos usá-los para escrever código assíncrono com promessas de uma forma síncrona mais familiar. Usando geradores e promessas de PHP, você pode se livrar completamente dos retornos de chamada. A idéia é que, quando cumprimos uma promessa (usando a chamada de rendimento), uma corrotina a assina. Corutin faz uma pausa e espera até que a promessa seja concluída (concluída ou cancelada). Assim que a promessa for concluída, a rotina continuará a ser cumprida. Após a conclusão bem-sucedida, a promessa da rotina envia o valor recebido de volta ao contexto do gerador usando a chamada Generator :: send ($ value) . Se a promessa falhar, Corutin lança uma exceção através do gerador usando a chamada Generator :: throw () . Na ausência de retornos de chamada, podemos escrever código assíncrono que se parece quase com o código síncrono usual.


Execução sequencial


Ao usar a corotina, a ordem de execução no código assíncrono agora importa. O código é executado exatamente no local em que a palavra-chave yield é chamada e pausada até que a promessa seja concluída. Considere o seguinte código:


 <?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: será exibido aqui , então a execução pausa e aguarda. Assim que a promessa do makeRequest ('url1') for concluída, imprimimos seu resultado e passamos para a próxima linha de código.


Tratamento de erros


O Padrão Promises / A + Promise declara que cada Promise contém os métodos then () e catch () . Essa interface permite criar cadeias a partir de promessas e, opcionalmente, capturar erros. Considere o seguinte código:


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

Aqui temos uma cadeia de promessas que passa o resultado de cada promessa anterior para a próxima. Mas não há bloco catch () nesta cadeia, não há tratamento de erros aqui. Quando uma promessa em uma cadeia falha, a execução do código é movida para o manipulador de erros mais próximo da cadeia. No nosso caso, isso significa que a promessa pendente será ignorada e quaisquer erros lançados desaparecerão para sempre. Com as corotinas, o tratamento de erros vem à tona. Se alguma operação assíncrona falhar, uma exceção será lançada:


 <?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; } }); 

Tornando legível o código assíncrono


Os geradores têm um efeito colateral realmente importante que podemos usar para controlar a assincronia e que resolve o problema de legibilidade do código assíncrono. É difícil para nós entender como o código assíncrono será executado devido ao fato de o thread de execução alternar constantemente entre diferentes partes do programa. No entanto, nosso cérebro funciona basicamente de forma síncrona e de rosca única. Por exemplo, planejamos nosso dia de maneira muito consistente: fazer um, depois outro e assim por diante. Mas o código assíncrono não funciona da maneira que nosso cérebro está acostumado a pensar. Mesmo uma simples cadeia de promessas pode não parecer muito legível:


 <?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'; }); 

Temos que desmontá-lo mentalmente para entender o que está acontecendo lá. Portanto, precisamos de um padrão diferente para controlar a assincronia. Em resumo, os geradores fornecem uma maneira de escrever código assíncrono para que pareça síncrono.


Promessas e geradores combinam o melhor dos dois mundos: obtemos código assíncrono com ótimo desempenho, mas, ao mesmo tempo, parece síncrono, linear e seqüencial. As corotinas permitem ocultar a assincronia, que já está se tornando um detalhe de implementação. E nosso código ao mesmo tempo parece que nosso cérebro está acostumado a pensar - linear e sequencialmente.


Se estamos falando sobre o ReactPHP , podemos usar a biblioteca RecoilPHP para escrever promessas na forma de corotina. No Amp, as corotinas estão disponíveis imediatamente.

Source: https://habr.com/ru/post/pt453296/


All Articles