Asynchronous PHP

Lukasz
5 min readMar 2, 2023

--

Photo by Ricardo Gomez Angel on Unsplash

It is not an article about the parallel extension (or any other one that brings the power of threads into the PHP) — as a matter of fact I don’t think (as for the time being) there are any good use cases for using threads in PHP (there are other patterns — regarding the PHP — that allow making this kind of thing done right). But! There are also tools available (like the one I’m going to share with you here) that are just hard to ignore! And it’s the point — you can already do (at least one thing) asynchronously in PHP — just out-of-the-box!

Consider such an example:

You want to make 4 requests to the external (one or 4 different) service(s) — what will be the “time cost” of all 4 requests?

Everybody knows but let me quickly remind the two possible here scenarios:

  1. we can process the requests synchronously — then the total time is the sum of all subsequent requests’ cost
  2. we can do them asynchronously — the total time will be equal to the time of the longest request

See the below diagram to visualise the scenarios assuming we have to make 4 requests where:

  • the response for the first one is immediate (we can ignore the delay)
  • the response for the second one comes after 1 second
  • the response for the third — 2 seconds
  • the response for the fourth — 3 seconds

What answer will be applicable to PHP?

Let’s check it (the explanation of the performed test you can find below):

➜ docker compose run php
Sequence: 0 .. 3
...
end
Woke up after 0 second(s) of sleep!
Woke up after 1 second(s) of sleep!
Woke up after 2 second(s) of sleep!
Woke up after 3 second(s) of sleep!

real 0m3.099s

3 seconds! Surprised? Keep reading :)

What has been exactly tested here?

As an “external service” I used the tool I call the “sleeping server” — you can send a request to that tool (service) with the number of seconds as a parameter and it will respond after the requested time (more info and examples can be found at the project’s Readme).

Tested PHP code:

$client = new GuzzleHttp\Client();

for($i = $sequenceStart; $i <= $sequenceStop; $i++) {
Utils::task(
fn() => $client->getAsync("{$sleepingUrl}/{$i}")
->then($printResponse)
->wait()
);
}
echo 'end' . PHP_EOL;

And all together has been bound by docker-compose:

services:
sleeping:
build: "https://github.com/lbacik/sleeping-server.git#main"

php:
build: ./php
environment:
SLEEPING_URL: "http://sleeping:3000"
depends_on:
- sleeping
...

The whole code can be found at: https://github.com/lbacik/php-async repository.

Lets compare the PHP (official php:8.1 image) results with the JS code.

The JS version of the code:

for (let i=sequenceStart; i<=sequenceStop; i++) {
fetch(`${sleepingUrl}/${i}`)
.then(response => response.text())
.then(result => console.log(result))
}
console.log('end')

And the test:

➜  docker compose run js
Sequence: 0 .. 3
...
end
Woke up after 0 second(s) of sleep!
Woke up after 1 second(s) of sleep!
Woke up after 2 second(s) of sleep!
Woke up after 3 second(s) of sleep!

real 0m3.182s

The results are almost the same :) !

Things to explain

I think there are two (regarding the PHP code):

  1. Why those requests are at all executed? (please notice the “end” string printed in the script’s output — it is the last line of the script!)
  2. Why those requests are executed asynchronously?

The first one is a bit weird — i mean — the answer (at least to me). Let me explain:

  • we start from Util::task() as it has been used in my example - check that method (it is declared in GuzzleHttp\Promise namespace)! You can find there something like $queue = self::queue()
  • follow this trace and check the queue() method. If the queue doesn’t exist it is created: $queue = new TaskQueue() (what happens in our case) — let’s open the TaskQueue constructor:
public function __construct($withShutdown = true)
{
if ($withShutdown) {
register_shutdown_function(function () {
if ($this->enableShutdown) {
// Only run the tasks if an E_ERROR didn't occur.
$err = error_get_last();
if (!$err || ($err['type'] ^ E_ERROR)) {
$this->run();
}
}
});
}
}

Voilà: register_shutdown_function!

It surprised me (a lot!) at first. But, yep, if there are such possibilities in PHP why not use them (however, I’m not sure about that 🤔).

And now:

Why those requests are executed asynchronously?

Because the curl library allows for that!

-Z, — parallel

Makes curl perform its transfers in parallel as compared to the regular serial manner.

And PHP also uses this library - the php interpreter can be compiled with it or it can be added as an extension — the PHP extensions can be checked by php -m:

$ php -m
...
curl
...

Curl PHP extension provides the access to the system curl library — the same you can use with the cli curl interface (if i have checked it correctly):

$ docker run --rm -ti php:8.1 bash
...
# ldd /usr/local/bin/php | grep curl
libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 ...
# ldd /usr/bin/curl | grep libcurl
libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 ...

It is a tool written in C (I guess) which can leverage the threads :) And the PHP API provides a way to utilise the multi-threads functionality provided by the libcurl library!

Here comes the next part — if you want to use this functionality directly, I mean in the way the PHP API describes it then the discussed code could look like this:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

$sleepingUrl = getenv('SLEEPING_URL') ?: "http://localhost:3000";

$multi = curl_multi_init();

$stillRunning = null;

for ($i = 0; $i <= 3; $i++) {
$curl = curl_init("{$sleepingUrl}/{$i}");
curl_multi_add_handle($multi, $curl);
}

do {
$status = curl_multi_exec($multi, $stillRunning);
if ($stillRunning) {
curl_multi_select($multi);
}
} while ($stillRunning && $status == CURLM_OK);

the above script can be found here and executed / tested by (sorry for that little cli-monster):

$ docker compose run \
--entrypoint "/bin/bash -c \"time php bin/fetch_mcurl.php\"" php

Yep — you can use it like that — but doesn’t it look much more complex than the solution presented above:

Utils::task(
fn() => $client->getAsync("{$sleepingUrl}/{$i}")
->then($printResponse)
->wait()
);

As for me, the usage through the promise interface implemented by Guzzle developers looks just much much better :)

Afterword

So “asynchronous PHP” is possible! 😉 Interesting whether there are any other “extensions” provides asynchronous functionalities? Have to check it one day (If you know any — let me know!) :)

--

--

Responses (7)