Managing Asynchrony in PHP: From Promises to Coroutines

Original author: Sergey Zhuk
  • Transfer
  • Tutorial


What is asynchrony? In short, asynchrony means performing several tasks within a certain period of time. PHP runs in a single thread, which means that only one piece of PHP code can be executed at any given time. This may seem like a limitation, but it actually gives us more freedom. As a result, we don’t have to face all the complexity associated with multithreaded programming. But on the other hand, there is a set of problems. We have to deal with asynchrony. We need to somehow manage it and coordinate it.


Introducing the translation of an article from the blog of Skyeng backend developer Sergey Zhuk.


For example, when we execute two parallel HTTP requests, we say that they are "running in parallel." This is usually easy and simple to do, but problems arise when we need to streamline the responses of these requests, for example, when one request requires data received from another request. Thus, it is in asynchrony management that the greatest difficulty lies. There are several different ways to solve this problem.


PHP currently does not have native support for high-level abstractions to control asynchrony, and we have to use third-party libraries such as ReactPHP and Amp. In the examples in this article, I use ReactPHP.

Promises


To better understand the idea of ​​promises, a real-life example will come in handy. Imagine that you are at McDonald's and want to place an order. You pay money for it and thus begin the transaction. In response to this transaction, you expect to get a hamburger and fries. But the cashier does not immediately return the food. Instead, you receive a check with the order number. Consider this check as a promise for future orders. Now you can take this check and start thinking about your delicious lunch. The expected hamburger and french fries are not ready yet, so you stand and wait until your order is completed. As soon as his number appears on the screen, you will exchange the check for your order. These are the promises:


Substitute for future value.

A promise is a representation for future meaning, a time-independent wrapper that we wrap around meaning. We don’t care if the value is already here or not. We continue to think of him the same. Imagine that we have three asynchronous HTTP requests that are executed “in parallel” so that they will be completed at about one point in time. But we want to somehow coordinate and organize their answers. For example, we want to print these answers as soon as they are received, but with one slight restriction: do not print the second answer until the first is received. Here I mean that if $ promise1 is fulfilled, then we print it. But if $ promise2 is fulfilled first, we don’t print it, because $ promise1still in progress. Imagine that we are trying to adapt three competitive queries in such a way that for the end user they look like one quick request.


So, how can we solve this problem with promises? First of all, we need a function that returns a promise. We can collect three such promises and then put them together. Here is some fake code for this:


<?phpuseReact\Promise\Promise;
functionfakeResponse(string $url, callable $callback){
    $callback("response for $url");
}
functionmakeRequest(string $url){
    returnnew Promise(function(callable $resolve)use($url){
        fakeResponse($url, $resolve);
    });
}

Here I have two functions:
fakeResponse (string $ url, callable $ callback) contains a hard-coded answer and allows the specified callback with this answer;
makeRequest (string $ url) returns a promise that uses fakeResponse () to indicate that the request has completed.


From the client code, we simply call the makeRequest () function and get the promises:


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

It was simple, but now we need to sort these answers somehow. Once again, we want the response from the second promise to be printed only after the completion of the first. To solve this problem, you can build a chain of promises:


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

In the above code, we start with $ promise1 . Once it is completed, we print its value. We don’t care how long it takes: less than a second or an hour. As soon as the promise is completed, we will print its value. And then we wait for $ promise2 . And here we can have two scenarios:


$ promise2 is already completed, and we immediately print its value;
$ promise2 is still being fulfilled, and we are waiting.


Thanks to the chaining of promises, we no longer need to worry about whether or not some promise has been fulfilled. Promis does not depend on time, and thereby it hides its states from us (in the process, already completed or canceled).


This is how you can control asynchrony with promises. And it looks great, the chain of promises is much prettier and more understandable than a bunch of nested callbacks.


Generators


In PHP, generators are built-in language support for functions that can be paused and then continued. When code execution inside such a generator stops, it looks like a small blocked program. But outside of this program, outside the generator, everything else continues to work. This is all the magic and power of generators.


We can literally pause the generator locally to wait for the promise to complete. The basic idea is to use promises and generators together. They take over the control of asynchrony, and we just call yield when we need to suspend the generator. Here is the same program, but now we are connecting generators and promises:


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

For this code, I use the library recoilphp / recoil , which allows you to call ReactKernel :: start () . Recoil provides the ability to use PHP generators to execute ReactPHP asynchronous promises.

Here, we are still doing three queries in parallel, but now we are sorting the responses using the yield keyword . And again, we display the results at the end of each promise, but only after the previous one.


Coroutines


Coroutines are a way of dividing an operation or process into chunks, with some execution inside each such chunk. As a result, it turns out that instead of performing the entire operation at a time (which can lead to a noticeable freezing of the application), it will be performed gradually until all the necessary amount of work has been completed.


Now that we have interruptible and renewable generators, we can use them to write asynchronous code with promises in a more familiar synchronous form. Using PHP generators and promises, you can completely get rid of callbacks. The idea is that when we give out a promise (using the yield call), a coroutine subscribes to it. Corutin pauses and waits until the promise is completed (completed or canceled). As soon as the promise is completed, coroutine will continue to fulfill. Upon successful completion, the coroutine promise sends the received value back to the generator context using the Generator :: send ($ value) call . If the promise fails, then the coroutine throws an exception through the generator using the Generator :: throw () call. In the absence of callbacks, we can write asynchronous code that looks almost like the usual synchronous one.


Sequential execution


When using coroutine, the execution order in asynchronous code now matters. The code is executed exactly to the place where the yield keyword is called and then paused until the promise is completed. Consider the following code:


<?phpuseRecoil\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: will be displayed here , then execution pauses and waits. As soon as the promise from makeRequest ('url1') is completed, we print its result and move on to the next line of code.


Error processing


The Promises / A + Promise Standard states that each Promise contains then () and catch () methods . This interface allows you to build chains from promises and optionally catch errors. Consider the following code:


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

Here we have a chain of promises that passes the result of each previous promise to the next. But there is no catch () block in this chain , there is no error handling here. When a promise in a chain fails, code execution moves to the nearest error handler in the chain. In our case, this means that the outstanding promise will be ignored, and any errors thrown out will disappear forever. With coroutines, error handling comes to the fore. If any asynchronous operation fails, an exception will be thrown:


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

Making Asynchronous Code Readable


Generators have a really important side effect that we can use to control asynchrony and which solves the problem of readability of asynchronous code. It’s hard for us to understand how asynchronous code will be executed due to the fact that the execution thread constantly switches between different parts of the program. However, our brain basically works synchronously and single-threaded. For example, we plan our day very consistently: to do one, then another, and so on. But asynchronous code does not work the way our brains are used to thinking. Even a simple chain of promises may not look very readable:


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

We have to mentally disassemble it in order to understand what is happening there. So we need a different pattern to control asynchrony. In short, generators provide a way to write asynchronous code so that it looks like synchronous.


Promises and generators combine the best of both worlds: we get asynchronous code with high performance, but at the same time it looks like synchronous, linear and sequential. Coroutines allow you to hide asynchrony, which becomes an implementation detail. And our code at the same time looks like our brain is used to thinking - linearly and sequentially.


If we are talking about ReactPHP , then the RecoilPHP library can be used to write promises in the form of coroutine. In Amp, coroutines are available right out of the box.


Also popular now: