Hi Abbas,

Thanks for putting a prototype behind the idea.

I think PHP is already closer to good async than it looks, and that the
best path keeps the property Fibers gave us: no function colouring. Any
function can suspend, and its caller doesn't need to know. That lets
PHP go the way of Go rather than JavaScript: code is written as
ordinary blocking code, and concurrency is chosen at the call site, not
declared on the function.

This runs today, unchanged, on phasync, Revolt, AMPHP, ReactPHP and
Swoole:

    use function fiber\{go, await_all, sleep};

    function fetchAll(array $ms): array
    {
        $tasks = [];
        foreach ($ms as $m) {
            $tasks[] = go(function () use ($m) {
                sleep($m / 1000);
                return "slept $m ms";
            });
        }
        return await_all($tasks);
    }

sleep() is just called, with no await. It suspends the task it runs
in, not the process, and fetchAll() is an ordinary function that its
callers can't tell apart from a blocking one.

The only reason it is fiber\sleep() and not \sleep() is that PHP's own
sleep() has no way to tell the scheduler it is about to block. Give
userland a way to be notified when a fiber would otherwise block, and
plain \sleep(), fread() or PDO::query() behave the same way. At that
point there is nothing left for an await on a function call to do.

Compare Async\delay() in your proposal. Without the await,

    $value = Async\delay($secs);

doesn't wait at all: $value is a Future, and the code carries on at
once. Every function that returns a Future has that trap, and every
caller has to know which functions are which. That is the colouring,
and it spreads: a library that wants to serve both kinds of code ends
up with two versions of each function.

I read your point that everyone still picks a userland library as a
frustration with interoperability, and I share it. A library written
for Amp doesn't run on ReactPHP. But that is solvable without new
syntax. The code above is from fiber-interop, a small common API with
an adapter per event loop:

https://fiber-interop.github.io/

Most of the remaining pieces exist too. PHP even lets userland make
file I/O transparent: phasync/file-streamwrapper replaces the file://
wrapper, so plain file_get_contents()/fread() code suspends only its
own fiber:

https://github.com/phasync/file-streamwrapper

That works because stream wrappers are a userland extension point.
PDO, curl, mysqli and the socket functions have no such point. What the
language lacks is the interface for them: a standard way for an
internal function that would block the whole process to ask the
userland scheduler to suspend the current fiber, and to be resumed when
its I/O is ready. With that, existing code becomes concurrent without
being rewritten, under whichever event loop the application uses.

If PHP gets async and await keywords some day, I think they should
launch and join fibers at the call site, not mark functions and await
futures:

    $fiber = async some_function(1, 2, 3);
    $result = await $fiber;

some_function() stays an ordinary function. Whether it runs
concurrently is the caller's choice, as with Go's "go", so nothing is
coloured.

A request that runs in many fibers also needs to know which request it
belongs to. That is Go's context.Context, and it's what I've just
proposed as the Scope RFC:

https://wiki.php.net/rfc/scope

Frode Børli

Reply via email to