PHP
Article

An Introduction into Event Loops in PHP

By Christopher Pitt

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!

  • Rookie

    Regarding “With the browser WebAPIs (things like setTimeout and addEventListener) we can offload parallel work to different threads.” it’s not quite true. JS is single threaded as well, the only multi-thread approach is with webworkers, but that’s different story.

    Very nice article, thanks !

  • http://www.kelunik.com/ kelunik

    You don’t need any extension or ‘obscure’ thing to create a simple event loop for your example. The only thing you need is a task scheduler. Task scheduling is something browsers do for us. PHP doesn’t have native support, so you have to implement an own task scheduler. These ‘obscure’ methods come into play when you’re dealing with non-blocking I/O. Otherwise, good article!

    • Mitch J.V.

      You do need an extension because using an event loop is only interesting when you deal with CLI, which is extremely useful for creating background services with PHP. For regular http requests and responses you don’t need an event loop exposed. Your comment is inaccurate and misleading.

      • http://blog.kelunik.com/ kelunik

        “You do need […] because it’s only interesting when …”, interesting reasoning. You don’t need an extension, PHP’s internal stream_select is fine for most cases as long as you don’t have many, many watchers.

        • Mitch J.V.

          My reasoning is valid. Your reasoning comes from POV of using PHP in HTTP environment. What happens if you do have many, many watchers, as you said? What happens then? Please, do finish the sentence. I see no reason why one shouldn’t use OS provided event loop interface. You do and your reason is that you can use the browser, which is fine, for certain use cases. For CLI / background services (let’s think various queues you’d be able to use) – the extension becomes very attractive. Given the fact an extension is trivial to install, even if compiled from source – then what? Please, do humor me with a sarcastic remark once more :)

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.