PHP - - By Christopher Pitt

An Introduction into Event Loops in PHP

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.

Pfeil1105a

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.

Where Things Go To Wait

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…

stack-error

We call that list of functions a stack trace. It’s what things look like inside the stack browsers use. They’ll handle this code in steps…

stack-animation

This is like how PHP uses a stack to store context. Browsers go a step further and provide WebAPIs 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 WebAPIs (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.

Life Without An 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

Icicle is library of components built with the event loop in mind. Let’s look at a simple example:

use Icicle\Loop;

Loop\timer(0.1, function() {
    print "inside timer";
});

print "outside timer";

Loop\run();

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 Icicle\Coroutine;
use Icicle\Dns\Resolver\Resolver;
use Icicle\Loop;

$coroutine = Coroutine\create(function ($query, $timeout = 1) {
    $resolver = new Resolver();

    $ips = (yield $resolver->resolve(
        $query, ["timeout" => $timeout]
    ));

    foreach ($ips as $ip) {
        print "ip: {$ip}\n";
    }
}, "sitepoint.com");

Loop\run();

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

ReactPHP has a similar event loop implementation, but without all the interesting generator stuff:

$loop = React\EventLoop\Factory::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!

Conclusion

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.

Stay tuned for a more practical implementation of Icicle, coming soon!

Sponsors