PHP Fibers RFC in PHP 8.1 vs Swoole Fibers and Coroutines

What are PHP Fibers, Swoole Coroutines, non-blocking IO and async PHP? What is the difference with PHP Fibers RFC and Swoole Coroutines?

In the PHP world, there are a lot of options for asynchronous programming but this wasn't always true for PHP. Developers have mostly used PHP in a blocking, synchronous stateless manner. Over the years since PHP 7 was released where we saw huge performance improvements to the core and many language additions and now PHP 8 with the new JIT (Just in Time) engine, all this has tackled a lot of the performance and programming issues PHP once had.

Most other popular web frameworks from other stacks have taken advantage of the event loop model, such as Node.js or Python's Tornado, enabling them to achieve higher throughput and concurrency, something which is important when scaling an application. But now, PHP has many options for taking advantage of the event loop model and asynchronous programming, something that once wasn't a thing in the PHP world.

Recently, a new PHP RFC has been passed to enable more support for asynchronous programming, the Fibers RFC, targeting PHP 8.1, which is very similar to the way Swoole Coroutines work, with that in mind it is good to go through the differences and understand how the PHP Fibers RFC compares against Swoole Coroutines, which can be classed as Fibers as well.

What are PHP Fibers?

The proposed Fiber RFC for PHP is a way of implementing coroutines (also known as green-threads), where code can be written synchronously but executed concurrently, a Fiber (or coroutine) is a lightweight thread of execution that achieves cooperative multitasking based on I/O. Cheap and lightweight because everything is kept within the same address space/thread, does not require switching between processes which have a great impact. The Fibers RFC has been introduced to provide a basic starting point for native asynchronous programming in PHP and only solves a part of what is required to truly take advantage of Fibers/coroutines.

What are Swoole Coroutines?

Swoole implements coroutines based on I/O switching and can be simply understood as a user-land thread, just like how Fibers work. Swoole coroutines are cheap to execute and lightweight when switching between different fibers/coroutines. Within Swoole, coroutines are managed by the coroutine scheduler and can automatically switch context based on I/O operations, for example, when waiting for a database query to return, another coroutine can be executed. Coroutines in Swoole are very similar to how goroutines work in Go-lang and does not rely on future/promise APIs, callbacks or async/await API.

PHP Fibers RFC Example

<?php
$fiber = new Fiber(function () 
{
    // Inside of fiber 1

    $anotherFiber = new Fiber(function()
    {
        // Inside of fiber 2
    });

    $value = $anotherFiber->start();
});

$value = $fiber->start();

Here we have a simple example of how we can create two fibers, one being nested inside the first one. These fibers will be executed concurrently based on when a fiber is suspended or finishes.

Swoole Coroutine Example

<?php
Co\run(function() 
{
    go(function() 
    {
        // Inside of coroutine 1

        go(function() 
        {
            // Inside of coroutine 2
        });
    });
});

With Swoole we can do the same thing by executing two fibers/coroutines concurrently but we have to wrap our coroutines with a call to Co\run which is the built-in coroutine scheduler Swoole uses to automatically handle context switching, allowing different coroutines to execute when waiting for another. By executing our Swoole fibers/coroutines inside of Co\run it creates a new coroutine container.

Why fibers/coroutines are important for PHP

Web applications or servers contain a lot of different functionality and usually are responsible for performing many different tasks, they handle requests which itself is like a small program until it sends back a response. During the time one request is handled it would be beneficial if the server could also handle another request at the same time, concurrently. Allowing progress to be made on both requests simultaneously based on when one or another is waiting for I/O operations. This speeds up requests dramatically and allows the developer to still write synchronous code.

Problems to Consider with fibers/coroutines

We know fibers/coroutines are good for executing tasks concurrently in a synchronous manner but using them comes with some considerations in order to properly utilise fibers/coroutines:

  • Switching Complexity: You must design and program with knowing when to suspend or resume a fiber/coroutine, usually must be left to framework or library authors

  • Fiber/coroutines synchronisation & Communication: Because fibers/coroutines have access to the same memory space, they can conflict with modifying memory which could be dependant on another fiber/coroutine. This can be solved with a range of solutions like mutexes, semaphores, memory parcels, and channels but this is low-level programming and not everyone will want to deal with this

  • Event Loops: In order to take full advantage of fibers/coroutines, they are best used within an event loop, these can be complex and sometimes very opinionated which may require different event loop implementations for different use cases, one event loop might not fit all programs thus, making it harder to adopt fibers/coroutines if no event loop is supported

  • PHP Ecosystem: Are the existing PHP ecosystem, PHP frameworks, PHP libraries supported with minimum modification or migration?

How does PHP Fibers RFC solve usage problems?

  • Switching Complexity: The Fibers RFC relies on the use of suspend or resume in order to handle context switching but it is more of a low-level PHP library or framework responsibility, meaning most developers won't have to deal with this complexity although good support for this is needed to take full advantage and performance you can get with Fibers

  • Fiber/coroutines synchronisation & Communication: It is proposed that problems with memory access and conflicts can be solved with what was mentioned above but the fibers RFC does not provide any solution because it is out of scope. It is also something that can be implemented in user code.

  • Event Loops: Because event loops can be very opinionated the Fibers RFC has left out any implementation of one and suggested a few 3rd party PHP frameworks which add support for them. Adding one to the core of PHP may not be needed as one solution might not fit all use cases.

  • PHP Ecosystem: The existing PHP ecosystem is not supported. In order to use PHP Fibers RFC, you have to rely on a few non-standard 3rd party PHP libraries then rebuild the ecosystem upon these non-standard 3rd party PHP libraries.

How does Swoole solve fiber/coroutine usage problems?

  • Switching Complexity: Swoole provides a fibers/coroutines scheduler that can manage context switching automatically based on I/O, replacing the need for using async/await. This kind of functionality is extremely important because it means PHP developers can enjoy the performance of async IO with the same sequential PHP codes and Swoole.

  • Fiber/coroutines synchronisation & Communication: There are a range of ways to handle Fiber/coroutines communication and Swoole provides you with Channels. Very similar to Go-lang channels they allow you to sync communication between different coroutines and prevent incorrect memory access or conflicts. Channels are built-in already with Swoole and provide an easy API to manage different channels, allowing you to pull and push data like an array.

  • Event Loops: Swoole provides its event loop API which is based on epoll_wait and manages all the I/O waiting and writing, switching between coroutines for you. Only non-blocking code should be added to the event loop. However, Swoole does provide an event loop for different use cases, such as the HTTP server or WebSocket Server.

  • PHP Ecosystem: Swoole is trying to support the existing PHP ecosystem, PHP frameworks and PHP libraries transparently. Swoole automatically turns the lower level blocking libraries to be non-blocking libraries with HOOKs. You can even use sleep, curl, file_get_contents, PDO, stream_socket_client within Swoole Coroutines and not block the event loop, while all these blocks the PHP Fibers RFC applications. You can find a list of libraries that can be used in Swoole at https://www.swoole.co.uk/docs/modules/swoole-runtime-flags.

The differences of a real-world PHP application

Swoole supports the native CURL you have already known and all the PHP libraries based on CURL.

The PHP Fibers RFC can only be used to make an HTTP Client with the use of a third-party PHP package.

Let's take a look.

Swoole coroutines HTTP Client with CURL

<?php

use Swoole\Coroutine\WaitGroup;

function request($url)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);

    $headers = array();
    $headers[] = 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36';
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $output = curl_exec($ch);
    if($output === false) 
    {
        echo "CURL Error:". curl_error($ch). " ". $url. "\n";
        return $resp;
    }

    curl_close($ch);
    return $output;
}

Co\run(function()
{

    $wg = new WaitGroup();

    $requests = [];

    go(function() use($wg, &$requests)
    {
        $wg->add();
        $url = 'https://www.swoole.co.uk';
        $requests[] = request($url);
        $wg->done();
    });

    go(function() use($wg, &$requests)
    {
        $wg->add();
        $url = 'https://php.net';
        $requests[] = request($url);
        $wg->done();
    });

    $wg->wait(1);
    var_dump($requests);
});

P.S. you can execute the above script with Swoole.

PHP Fibers RFC HTTP Client

<?php

$loop = new FiberLoop(Factory::create());

$browser = new Browser($loop);

$request = function (string $method, string $url) use ($browser, $loop): void
{
    /** @var Response $response */
    $response = $loop->await($browser->requestStreaming($method, $url));

    /** @var ReadableStreamInterface $stream */
    $stream = $response->getBody();

    $body = $loop->await(Stream\buffer($stream));

    var_dump(\sprintf(
        '%s %s; Status: %d; Body length: %d',
        $method,
        $url,
        $response->getStatusCode(),
        \strlen($body)
    ));
};

$requests = [];

$requests[] = $loop->async($request, 'GET', 'https://www.swoole.co.uk');
$requests[] = $loop->async($request, 'GET', 'https://php.net');

$loop->await(Promise\all($requests));

P.S. the script with PHP Fibers RFC can be executed is more complex.

You have to learn and understand these new features and API in a non-strand third-party PHP library: FiberLoop, await, async.

The last question for PHP developers to consider is: do you like to remove the native curl, sleep, MySQL PDO, Redis in your frameworks and applications, then use a third-party library written with PHP maybe providing the similar features of curl, sleep, PDO?