PHP developers are always waiting for something. Sometimes we’re waiting for requests to remote services. Sometimes we’re waiting for databases to return rows from a complex query. Wouldn’t it be great if we could do other things during all that waiting?
If you’ve written some JS, you’re probably familiar with callbacks and DOM events. And though we have callbacks in PHP, they don’t work in quite the same way. That’s thanks to a feature called the event loop.
We’re going to look at how the event loop works, and how we can use the event loop in PHP.
We’re going to see some interesting PHP libraries. Some would consider these not yet stable enough to use in production. Some would consider the examples presented as “better to do in more mature languages”. There are good reasons to try these things. There are also good reasons to avoid these things in production. The purpose of this post is to highlight what’s possible in PHP.
To understand event loops, let’s look at how they work in the browser. Take a look at this example:
function fitToScreen(selector) {
var element = document.querySelector(selector);
var width = element.offsetWidth;
var height = element.offsetHeight;
var top = "-" + (height / 2) + "px";
var left = "-" + (width / 2) + "px";
var ratio = getRatio(width, height);
setStyles(element, {
"position": "absolute",
"left": "50%",
"top": "50%",
"margin": top + " 0 0 " + left,
"transform": "scale(" + ratio + ", " + ratio + ")"
});
}
function getRatio(width, height) {
return Math.min(
document.body.offsetWidth / width,
document.body.offsetHeight / height
);
}
function setStyles(element, styles) {
for (var key in styles) {
if (element.style.hasOwnProperty(key)) {
element.style[key] = styles[key];
}
}
}
fitToScreen(".welcome-screen");
This code requires no extra libraries. It will work in any browser that supports CSS scale transformations. A recent version of Chrome should be all you need. Just make sure the CSS selector matches an element in your document.
These few functions take a CSS selector and center and scale the element to fit the screen. What would happen if we threw an Error
inside that for
loop? We’d see something like this…
We call that list of functions a stack trace. It’s what things look like inside the stack browsers use.
This is like how PHP uses a stack to store context. Browsers go a step further and provide APIs for things like DOM events and Ajax callbacks. In its natural state, JavaScript is every bit as asynchronous as PHP. That is: while both look like they can do many things at once, they are single threaded. They can only do one thing at a time.
With the browser APIs (things like setTimeout and addEventListener) we can offload parallel work to different threads. When those events happen, browsers add callbacks to a callback queue. When the stack is next empty, browses pick the callbacks up from the callback queue and execute them.
This process of clearing the stack, and then the callback queue, is the event loop.
In JS, we can run the following code:
setTimeout(function() {
console.log("inside the timeout");
}, 1);
console.log("outside the timeout");
When we run this code, we see outside the timeout
and then inside the timeout
in the console. The setTimeout
function is part of the WebAPIs that browsers give us to work with. When 1 millisecond has passed, they add the callback to the callback queue.
The second console.log
completes before the one from inside the setTimeout
starts. We don’t have anything like setTimeout
in standard PHP, but if we had to try and simulate it:
function setTimeout(callable $callback, $delay) {
$now = microtime(true);
while (true) {
if (microtime(true) - $now > $delay) {
$callback();
return;
}
}
}
setTimeout(function() {
print "inside the timeout";
}, 1);
print "outside the timeout";
When we run this, we see inside the timeout
and then outside the timeout
. That’s because we have to use an infinite loop inside our setTimeout
function to execute the callback after a delay.
It may be tempting to move the while
loop outside of setTimeout
and wrap all our code in it. That might make our code feel less blocking, but at some point we’re always going to be blocked by that loop. At some point we’re going to see how we can’t do more than a single thing in a single thread at a time.
While there is nothing like setTimeout
in standard PHP, there are some obscure ways to implement non-blocking code alongside event loops. We can use functions like stream_select
to create non-blocking network IO. We can use C extensions like EIO to create non-blocking filesystem code. Let’s take a look at libraries built on these obscure methods…
Icicle is library of components built with the event loop in mind. Let’s look at a simple example:
function setTimeout(callable $callback, $delay) {
$now = microtime(true);
while (true) {
if (microtime(true) - $now > $delay) {
$callback();
return;
}
}
}
setTimeout(function() {
print "inside the timeout";
}, 1);
print "outside the timeout";
This is with icicleio/icicle
version 0.8.0
.
Icicle’s event loop implementation is great. It has many other impressive features; like A+ promises, socket, and server implementations.
Icicle also uses generators as co-routines. Generators and co-routines are a different topic, but the code they allow is beautiful:
use IcicleCoroutine;
use IcicleDnsResolverResolver;
use IcicleLoop;
$coroutine = Coroutinecreate(function ($query, $timeout = 1) {
$resolver = new Resolver();
$ips = (yield $resolver->resolve(
$query, ["timeout" => $timeout]
));
foreach ($ips as $ip) {
print "ip: {$ip}
";
}
}, "sitepoint.com");
Loop
un();
This is with icicleio/dns
version 0.5.0
.
Generators make it easier to write asynchronous code in a way that resembles synchronous code. When combined with promises and an event loop, they lead to great non-blocking code like this!
ReactPHP has a similar event loop implementation, but without all the interesting generator stuff:
$loop = ReactEventLoopFactory::create();
$loop->addTimer(0.1, function () {
print "inside timer";
});
print "outside timer";
$loop->run();
This is with react/event-loop
version 0.4.1
.
ReactPHP is more mature than Icicle, and it has a larger range of components. Icicle has a way to go before it can contend with all the functionality ReactPHP offers. The developers are making good progress, though!
It’s difficult to get out of the single-threaded mindset that we are taught to have. We just don’t know the limits of code we could write if we had access to non-blocking APIs and event loops.
The PHP community needs to become aware of this kind of architecture. We need to learn and experiment with asynchronous and parallel execution. We need to pirate these concepts and best-practices from other languages who’ve had event loops for ages, until “how can I use the most system resources, efficiently?” is an easy question to answer with PHP.
3.145.107.66