Cara menggunakan php fibers rfc

You will receive an email on last Wednesday of every month and on major PHP releases with new articles related to PHP, upcoming changes, new features and what's changing in the language. No marketing emails, no selling of your contacts, no click-tracking, and one-click instant unsubscribe from any email you receive.

Pur potendo contare su progetti come ReactPHP e Guzzle, fino ad ora PHP si è evoluto nativamente come un linguaggio basato su codice sincrono, questo significa ad esempio che l'esecuzione di una funzione viene interrotta fino alla restituzione di un risultato.

Tale dinamica può essere spiegata a larghe linee tramite il problema chiamato "Di che colore è la tua funzione?". Se infatti assumessimo che ogni funzione debba essere associata ad un colore e che il modo in cui avviene la chiamata ad una funzione dipenda da questo dato, avremmo l'unica possibilità di chiamare una funzione di un determinato colore da un'altra funzione dello stesso colore.

Funzioni sincrone e asincrone

Il meccanismo descritto non è forse intuitivo ma fondamentalmente ciò avviene perché le funzioni sincrone restituiscono valori mentre quelle asincrone invocano callback, nello stesso modo le funzioni sincrone possono offrire il loro risultato sotto forma di valore di ritorno mentre il risultato delle asincrone dipende sempre dal callback. Rispetto ad una sincrona una funzione asincrona modifica inoltre il modo in cui una funzione deve essere chiamata.

Ora abbiamo che le funzioni sincrone non possono chiamare le asincrone mentre può avvenire il contrario, ma per la chiamata ad una funzione asincrona è necessario che l'intero stack di chiamata sia asincrono.

Per questa ragione, utilizzando un linguaggio che la supporta, se una funzione restituisce una promise in uno stack di chiamata il risultato non potrà essere noto fino alla risoluzione della promise stessa, per far questo però l'intero stack deve poter restituire una promise.

Fiber e funzioni asincrone

Per rimediare al limite dovuto alla mancanza di codice asincrono, PHP 8.1 introduce il concetto di Fiber a cui fanno riferimento una classe omonima, una di riflessione (ReflectionFiber) e due classi per le eccezioni, FiberError e FiberExit.

Le Fiber sono state implementate per rimuovere la differenza tra codice sincrono e asincrono consentendo di interrompere le funzioni senza che ciò coinvolga il call stack nel suo insieme. In pratica le Fiber mettono in pausa lo stack di esecuzione, quindi per le chiamate dirette delle funzioni non si deve modificare il modo in cui esse sono invocate. Di seguito l'esempio riportato nella RFC di PHP 8.1.

final class Fiber
{
    public function __construct(callable $callback) {}
    public function start(mixed ...$args): mixed {}
    public function resume(mixed $value = null): mixed {}
    public function throw(Throwable $exception): mixed {}
    public function isStarted(): bool {}
    public function isSuspended(): bool {}
    public function isRunning(): bool {}
    public function isTerminated(): bool {}
    public function getReturn(): mixed {}
    public static function this(): ?self {}
    public static function suspend(mixed $value = null): mixed {}
}

new Fiber genera un oggetto Fiber mentre Fiber::suspend() sospende l'esecuzione della Fiber corrente e riprende l'esecuzione tramite la chiamata a Fiber->start(), Fiber->resume() o Fiber->throw().

Fiber->resume() può riattivare una Fiber restituendo un valore da Fiber::suspend() mentre Fiber->throw() può fare lo stesso se viene intercettata un'eccezione.

ReflectionFiber3 restituisce un valore di ritorno dalla conclusione della Fiber mentre ReflectionFiber4 fornirà l'istanza della in esecuzione o ReflectionFiber5 se chiamato da ReflectionFiber6.

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.

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.

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
$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.

<?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.

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.

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?

  • 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.

  • 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

    <?php
    Co\run(function()
    {
        go(function()
        {
            // Inside of coroutine 1
    
            go(function()
            {
                // Inside of coroutine 2
            });
        });
    });
    
    0,
    <?php
    Co\run(function()
    {
        go(function()
        {
            // Inside of coroutine 1
    
            go(function()
            {
                // Inside of coroutine 2
            });
        });
    });
    
    1,
    <?php
    Co\run(function()
    {
        go(function()
        {
            // Inside of coroutine 1
    
            go(function()
            {
                // Inside of coroutine 2
            });
        });
    });
    
    2,
    <?php
    Co\run(function()
    {
        go(function()
        {
            // Inside of coroutine 1
    
            go(function()
            {
                // Inside of coroutine 2
            });
        });
    });
    
    3,
    <?php
    Co\run(function()
    {
        go(function()
        {
            // Inside of coroutine 1
    
            go(function()
            {
                // Inside of coroutine 2
            });
        });
    });
    
    4 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://openswoole.com/docs/modules/swoole-runtime-flags.

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.

<?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://openswoole.com';
        $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

$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://openswoole.com');
$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

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

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

        go(function()
        {
            // Inside of coroutine 2
        });
    });
});
0, MySQL
<?php
Co\run(function()
{
    go(function()
    {
        // Inside of coroutine 1

        go(function()
        {
            // Inside of coroutine 2
        });
    });
});
3,
<?php
Co\run(function()
{
    go(function()
    {
        // Inside of coroutine 1

        go(function()
        {
            // Inside of coroutine 2
        });
    });
});
8 in your frameworks and applications, then use a third-party library written with PHP maybe providing the similar features of
<?php
Co\run(function()
{
    go(function()
    {
        // Inside of coroutine 1

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

        go(function()
        {
            // Inside of coroutine 2
        });
    });
});
0,
<?php
Co\run(function()
{
    go(function()
    {
        // Inside of coroutine 1

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