From this article you will learn:
- What problems, can be caused by rewriting one functionality to an asynchronous one
- What is the new mechanism in PHP 8.1 - Fibers
- How Fibers, can help you in introducing asynchronicity into your project
Asynchronicity in PHP
The language that is PHP has accustomed us to single-threaded programming. Most often, we call functions sequentially in such a way that each successive function waits to start until the previous one returns a result. However, the code does not have to be executed line by line, in a single thread.
Version four of the language introduced the possibility of creating asynchronous code. At first, the only possibility was to create forks through the pcntl_fork
function. Happily, today few people use this method, which involved rather complicated management of multiple processes. This method, built into the PHP language, split an existing process into two separate ones. Communication between these processes was very difficult, and it was easy to take up too many resources of the machine on which these processes were executed. Many libraries were created to make it easier, a good example of which is the spatie/async library.
In PHP 5.5, the keyword yield was added. This word allows you to create a generator. They allow non-linear code execution. As a result, many libraries supporting asynchronous programming were created. An example is Guzzle/promises.
Let's look at the difference between synchronous and asynchronous approaches in PHP. To do this, let's use an example:suppose we want to download the contents of three pages and combine them into one variable. Our synchronous code could look like this:
1$sites = [ 2 'https://sages.pl/', 3 'https://sages.pl/szkolenia', 4 'https://sages.pl/blog', 5]; 6 7function downloadSite($siteUrl) 8{ 9 $client = new GuzzleHttp\Client(); 10 return $client->get($siteUrl)->getBody(); 11} 12 13function downloadAllSites($sites): string 14{ 15 $sitesContent = []; 16 foreach ($sites as $siteUrl) { 17 $sitesContent[] = downloadSite($siteUrl); 18 } 19 20 return implode('<br>', $sitesContent); 21} 22 23 24$return = downloadAllSites($sites);
As you can see, we download the content of each site one by one. Finally, we combine everything into one. If we would like the download to work asynchronously, just replace the get
method with getAsync
. This method returns us a Promise
object. This object represents the result that will be returned when the asynchronous function is called.
1function downloadSite($siteUrl) 2{ 3 $client = new GuzzleHttp\Client(); 4 return $client->getAsync($siteUrl); 5} 6 7function downloadAllSites($sites) 8{ 9 $results = []; 10 foreach ($sites as $siteUrl) { 11 $results[] = downloadSite($siteUrl); 12 } 13 14 //return ?? 15 //i co tu możemy zwrócić? 16}
Unfortunately, having returned promise objects, we can't merge them until all these functions execute just as we can't return a promise object. This problem is well described in the article What color is your function?
In very simple terms, the aforementioned article describes the advantages and disadvantages of asynchronous programming. It warns against asynchronicity, pointing out, first of all, that calling an asynchronous function forces us to treat the entire function stack as asynchronous. The answer to this problem, in PHP 8.1, is Fibers - a solution, already successfully used in Ruby, which allows us to apply asynchronicity without rewriting the entire code. Of course, as you can see in the example above, we could wait for our asynchronous functions to execute by calling the wait
method on them, but I'm skipping this solution, because by doing so we lose the benefits that come with asynchronicity.
Fibers
Fibers represent interruptible functions. They can be interrupted at any time and remain suspended until resumed. This is best represented by an example from the PHP documentation:
1$fiber = new Fiber(function (): void { 2 $value = Fiber::suspend('fiber'); 3 echo "Value used to resume fiber: ", $value, PHP_EOL; 4}); 5 6$value = $fiber->start(); 7 8echo "Value from fiber suspending: ", $value, PHP_EOL; 9 10$fiber->resume('test');
This example will show us:Value from fiber suspending: fiber**Value used to resume fiber: test
Let's take a look at what's going on one by one in this example. We pass a callback to the constructor of the Fibers
class, which will be called when the start()
method is run. The key in this function is the call to Fiber::suspend(
). This interrupts the function passed in the constructor. The function is resumed only when the resumena
method of the fiber
object is called. Other interesting methods of the Fibers
class are:
isTerminated
, telling us whether the callback passed to the constructor has already executed,getReturn
, returning the same as the callback after execution.
When I originally saw this example, I didn't quite understand what its use might be. I found the answer only by reading the RFC. Fibers are created so that asynchronous functions can be called, without rewriting the entire function call stack. So this is the answer to the problem described at the beginning of this post and in the article What color is your function?
Let's try writing asynchronous code that could just as well be inside a synchronous function:
1use Spatie\Async\Pool; 2 3$fiber = new Fiber(function (): string { 4 $processes = [ 5 'operacja 1', 6 'operacja 2', 7 'operacja 3', 8 ]; 9 10 $pool = Pool::create(); 11 12 $result = new stdClass(); 13 $result->result = ''; 14 foreach ($processes as $processName) { 15 $pool->add(function() use($processName){ 16 $operationTime = rand(1, 15); 17 sleep($operationTime); 18 return $processName . ' zajeła ' . $operationTime . ' sekund' . PHP_EOL; 19 }) 20 ->then(function($output) use ($result) { 21 $result->result .= $output; 22 }); 23 } 24 25 while (count($pool->getFinished()) !== count($processes)) { 26 Fiber::suspend(); 27 $pool->notify(); 28 } 29 30 return $result->result; 31 32}); 33 34$value = $fiber->start(); 35 36while ($fiber->isTerminated() === false) { 37 sleep(1); 38 echo 'W tym miejscu w kodzie, możesz dokonać dowolną operację'. PHP_EOL; 39 $fiber->resume(); 40} 41 42echo $fiber->getReturn();
Every now and then, our code checks whether the asynchronous calls have already executed. And in the meantime, it allows us to execute our own code. This example, on standard output, will show us more or less this result:
1W tym miejscu w kodzie, możesz dokonać dowolną operację 2W tym miejscu w kodzie, możesz dokonać dowolną operację 3W tym miejscu w kodzie, możesz dokonać dowolną operację 4W tym miejscu w kodzie, możesz dokonać dowolną operację 5W tym miejscu w kodzie, możesz dokonać dowolną operację 6W tym miejscu w kodzie, możesz dokonać dowolną operację 7W tym miejscu w kodzie, możesz dokonać dowolną operację 8W tym miejscu w kodzie, możesz dokonać dowolną operację 9operacja 1 zajeła 4 sekund 10operacja 3 zajeła 5 sekund 11operacja 2 zajeła 7 sekund
Summary
Fibers are a little known and little described mechanism of PHP. If we go deeper into the subject, it turns out that they give us a solution to a fairly typical problem in asynchronous programming. Thanks to them, we can add asynchronicity at one point in our application, without having to rewrite the entire application. Unfortunately, most PHP programmers claim that they do not know what "Fibers" are used for. I asked such a question on Twitter and this is the response I got:
Happily, as the RFC developers emphasize, the Fibre interface is not expected to be used directly in the application code. Fibers provide basic and low-level flow control in the application. Thus, they are more suitable for use in libraries such as spatie/async or ReactPHP. They will allow the user to reap the benefits of Fibers indirectly.