PHP 8.1: Fibers

Version8.1
TypeNew Feature

Fibers is a new feature in PHP 8.1 that brings lightweight and controlled concurrency to PHP.

In essence, a Fiber is a code block that maintains its own stack (variables and state), that can be started, suspended, or terminated cooperatively by the main code and the Fiber.

Heads-up! This is page is rather long, and here is a jump-list for easier navigation.

Fibers are similar to threads in a computer program. Threads are scheduled by the operating system, and does not guarantee when and at which point the threads are paused and resumed. Fibers are created, started, suspended, and terminated by the program itself, and allows fine control of the main program execution and the Fiber execution.

PHP 5.5 added Generators to PHP. With Generators, it was possible to yield a Generator instance back to the caller, without deleting the state of the code block. Generators did not allow the call to be easily resumed from the point of the code block that yield was called.

With Fibers, the code block within the Fiber can suspend the code block and return any data back to the main program. The main program can resume the Fiber from the point it was suspended.

It is important the concurrent execution does not mean simultaneous execution. The Fiber and the main execution flow does not happen at the same time. It is up to the main execution flow to start a Fiber, and when it starts, the Fiber is executed exclusively. The main thread cannot observe, terminate, or suspend a Fiber while the Fiber is being executed. The Fiber can suspend itself, and it cannot resume by itself — the main thread must resume the Fiber.

Fiber flow

Fiber by itself does not allow simultaneous execution of multiple Fibers or the main thread and a Fiber.

Fiber class

PHP Fibers are implemented around a new class called \Fiber. This class is declared final, which prevents it from being extended by another user-land class.

<?php

final class Fiber
{
    /**
     * @param callable $callback Function to invoke
     * when starting the fiber.
     */
    public function __construct(callable $callback) {}

    /**
     * Starts execution of the fiber.
     * Returns when the fiber suspends or terminates.
     *
     * @param mixed ...$args Arguments passed to
     *   fiber function.
     *
     * @return mixed Value from the first suspension
     *   point or NULL if the fiber returns.
     *
     * @throw FiberError If the fiber has already
     *   been started.
     * @throw Throwable If the fiber callable throws
     *   an uncaught exception.
     */
    public function start(mixed ...$args): mixed {}

    /**
     * Suspend execution of the fiber. The fiber may be
     *   resumed with {@see Fiber::resume()} or
     *   {@see Fiber::throw()}.
     *
     * Cannot be called from {main}.
     *
     * @param mixed $value Value to return from
     *   {@see Fiber::resume()} or {@see Fiber::throw()}.
     *
     * @return mixed Value provided to
     *   {@see Fiber::resume()}.
     *
     * @throws FiberError Thrown if not within a
     *   fiber (i.e., if called from {main}).
     * @throws Throwable Exception provided to
     *   {@see Fiber::throw()}.
     */
    public static function suspend(mixed $value = null): mixed {}

    /**
     * Resumes the fiber, returning the given value
     *   from {@see Fiber::suspend()}.
     * Returns when the fiber suspends or terminates.
     *
     * @param mixed $value
     *
     * @return mixed Value from the next suspension
     *   point or NULL if the fiber returns.
     *
     * @throw FiberError If the fiber has not
     *   started, is running, or has terminated.
     * @throw Throwable If the fiber callable throws
     *   an uncaught exception.
     */
    public function resume(mixed $value = null): mixed {}

    /**
     * @return self|null Returns the currently executing
     *   fiber instance or NULL if in {main}.
     */
    public static function getCurrent(): ?self {}

    /**
     * @return mixed Return value of the fiber
     *   callback. NULL is returned if the fiber does
     *   not have a return statement.
     *
     * @throws FiberError If the fiber has not terminated
     *   or the fiber threw an exception.
     */
    public function getReturn(): mixed {}

    /**
     * Throws the given exception into the fiber
     *   from {@see Fiber::suspend()}.
     * Returns when the fiber suspends or terminates.
     *
     * @param Throwable $exception
     *
     * @return mixed Value from the next suspension
     *   point or NULL if the fiber returns.
     *
     * @throw FiberError If the fiber has not started,
     *   is running, or has terminated.
     * @throw Throwable If the fiber callable throws
     *   an uncaught exception.
     */
    public function throw(Throwable $exception): mixed {}

    /**
     * @return bool True if the fiber has been started.
     */
    public function isStarted(): bool {}

    /**
     * @return bool True if the fiber is suspended.
     */
    public function isSuspended(): bool {}

    /**
     * @return bool True if the fiber is currently running.
     */
    public function isRunning(): bool {}

    /**
     * @return bool True if the fiber has completed
     *   execution (returned or threw).
     */
    public function isTerminated(): bool {}
}

Fiber Class Methods


Fiber::__construct

When a new Fiber class instance is instantiated, the caller must pass a valid callable. It is possible to use local variable available in the scope as well.

new Fiber('var_dump');
new Fiber(fn(string $message) => print $message);
new Fiber(function(string $message): void {
    print $message;
});

The parameters of the callback will receive the exact same parameters that the Fiber::start() method is called with.

Fiber::start(): Start Fiber

Once a Fiber is created, it is not immediately started.

The Fiber::start() method call starts the callback set in Fiber::construct. All values passed to the Fiber::start method are passed to the callback.

$fiber = new Fiber(fn(string $message) => print $message);
$fiber->start('Hi');
Hi

Fiber::suspend(): Suspend a running Fiber

Fiber::suspend() is a static method, that must only be called from within the Fiber. It can optionally return a value, that the caller of Fiber::start() or Fiber::resume() can receive.

$fiber = new Fiber(function() {
    Fiber::suspend(42);
});
$return = $fiber->start();
echo $return;
42

When Fiber::suspend() is called, the Fiber is paused at that expression. The local variables, array pointers, etc. are not cleared until the Fiber object itself is removed from memory. The next Fiber::resume call continues the program from the next expression.

Calling Fiber::suspend() from outside a Fiber throws a FiberError exception:

$fiber = new Fiber(function() {});
Fiber::suspend();
FiberError: Cannot suspend outside of a fiber in ...:...

Fiber::resume: Resume a suspended Fiber

A Fiber that is suspended (with Fiber::suspend) can be resumed with the Fiber::resume method.

$fiber = new Fiber(function() {
    echo "Suspending...\n";
    Fiber::suspend();
    echo "Resumed!";
});
$fiber->start();
echo "Resuming...\n";
$fiber->resume();
Suspending...
Resuming...
Resumed!

The Fiber::resume method accepts a value, that can be assigned back to the return value of the last Fiber::suspend return value in the Fiber scope.

$fiber = new Fiber(function() {
    $last = Fiber::suspend(16);
    echo "Resuming with last value {$last}\n";
});
$last = $fiber->start();
echo "Suspended with last value {$last}\n";
$fiber->resume(42);
Suspended with last value 16
Resuming with last value 42

Calling Fiber::resume on a Fiber that is not suspended, or already terminated causes a FiberError.

$fiber = new Fiber(function() {});
$fiber->resume();
FiberError: Cannot resume a fiber that is not suspended in ...:...

Fiber::getCurrent: Get current Fiber instance

Fiber::getCurrent() static method returns the Fiber instance that is currently being run. This comes handy because $this is already assigned to the callable instance itself.

Using Fiber::getCurrent, it is possible for the Fiber to inspect its own state using methods such as Fiber::getReturn, Fiber::throw, Fiber::isStarted, Fiber::isSuspended, Fiber::isRunning, and [Fiber::isTerminated].

$fiber = new Fiber(function(): int {
    return 42;
});
$fiber->start();
var_dump($fiber->getReturn());
int(42)

If the Fiber callback does not return, getReturn() method returns null.

Calling getReturn on a Fiber that has not terminated, or threw a Throwable results in a FiberError exception:

$fiber = new Fiber(function(): void {
    Fiber::suspend();
});
$fiber->start();
$fiber->getReturn();
FiberError: Cannot get fiber return value: The fiber has not returned in ...:...

Fiber::throw: Throw to Fiber

Fiber::throw() method accepts a \Throwable object that resumes the Fiber but also immediately throws that Exception.

If the Fiber that called Fiber::suspend can optionally catch the passed \Throwable.

$fiber = new Fiber(function(): void {
    try {
        Fiber::suspend();
    }
    catch (Exception $ex) {
        echo $ex->getMessage();
    }
    echo 'Finishing';
});
$fiber->start();
$fiber->throw(new Exception("Test\n"));
Test
Finishing

If the Fiber callback does not catch the passed \Throwable object, it will bubble back to the caller.

$fiber = new Fiber(function(): void {
    Fiber::suspend();
    echo 'Finishing';
});
$fiber->start();
$fiber->throw(new Exception("Test\n"));
Fatal error: Uncaught Exception: Test

Note that Fiber::throw resumes the Fiber. If the Fiber catches the passed \Throwable object, it can continue. Calling Fiber::throw on a running Fiber causes the same error as calling Fiber::resume on a Fiber that is not suspended.


Fiber::isStarted

Returns whether the Fiber has started (with Fiber::start). This holds truth even if the Fiber is suspended or terminated.

$fiber = new Fiber(function(): void {});
$fiber->isStarted(); // false
$fiber->start();
$fiber->isStarted(); // true

Fiber::isSuspended

This rather self-explanatory method returns true if the Fiber is currently suspended.

$fiber = new Fiber(function(): void {
    Fiber::suspend();
});

$fiber->isSuspended(); // false
$fiber->start();
$fiber->isSuspended(); // true
$fiber->resume();
$fiber->isSuspended(); // false

Fiber::isRunning

A Fiber that is currently running returns true from Fiber::isRunning method.

Suspended and terminated states return false for Fiber::isRunning.

Fiber::isTerminated

Returns whether the Fiber callback has ended.

$fiber = new Fiber(function(): void {
    Fiber::suspend();
});

$fiber->isTerminated(); // false
$fiber->start();
$fiber->isTerminated(); // false
$fiber->resume();
$fiber->isTerminated(); // true

Summary of Fiber States


Fiber Exceptions

Fiber feature in PHP 8.1 adds two new Throwable classes. None of them can be instantiated by user-land PHP code because their execution is restricted at their constructor.

FiberError

/**
 * Exception thrown due to invalid fiber actions, such as resuming a terminated fiber.
 */
final class FiberError extends Error
{
    /**
     * Constructor throws to prevent user code from throwing FiberError.
     */
    public function __construct()
    {
        throw new \Error('The "FiberError" class is reserved for internal use and cannot be manually instantiated');
    }
}

FiberExit

/**
 * Exception thrown when destroying a fiber. This exception cannot be caught by user code.
 */
final class FiberExit extends Exception
{
    /**
     * Constructor throws to prevent user code from throwing FiberExit.
     */
    public function __construct()
    {
        throw new \Error('The "FiberExit" class is reserved for internal use and cannot be manually instantiated');
    }
}

Hierarchy of PHP exceptions An overlook of PHP core Exceptions including a chart on how PHP core exception classes are inherited.


Usage Examples

Fibers allow running concurrent execution of code, that can be suspended by the Fiber anytime, and optionally return values as well. From the main thread, it is possible to resume a suspended Fiber exactly where it was last suspended.

Note that Fibers added in PHP 8.1 is merely for the concurrency, but it does not enable parallel processing. For example, it will not allow running two Curl file downloads at the same time. Fibers can help as the underlying structures for a parallel processing event loop to easily manage the program state.

A Simple Echo Program

Following is a simple program that shows the flow of execution.

When Fiber::suspend() is a called, the Fiber is suspended right at the expression. It is possible to return a value at this point as well. If the Fiber does not call Fiber::suspend(), or throw, that Fiber is executed until it reaches the end of the callback.

It is entirely up to the main program to resume a suspended/thrown Fiber. If the main program exits, all remaining Fibers are discarded.

$fiber = new Fiber(function(): void {
    echo "Hello from the Fiber...\n";
    Fiber::suspend();
    echo "Hello again from the Fiber...\n";
});

echo "Starting the program...\n";
$fiber->start();
echo "Taken control back...\n";
echo "Resuming Fiber...\n";
$fiber->resume();
echo "Program exits...\n";

Output:

Starting the program...
Hello from the Fiber...
Taken control back...
Resuming Fiber...
Hello again from the Fiber...
Program exits...

A File Copy Program with Progress Bar

A simple echo example probably does not show the advantages of a Fiber because it does not return or pass any values.

Using Fibers, a simple program that copies a list of files to a destination can be made cleaner.

function writeToLog(string $message): void {
    echo $message . "\n";
}
$files = [
    'src/foo.png' => 'dest/foo.png',
    'src/bar.png' => 'dest/bar.png',
    'src/baz.png' => 'dest/baz.png',
];

$fiber = new Fiber(function(array $files): void {
    foreach($files as $source => $destination) {
        copy($source, $destination);
        Fiber::suspend([$source, $destination]);
    }
});

// Pass the files list into Fiber.
$copied = $fiber->start($files);
$copied_count = 1;
$total_count  = count($files);

while(!$fiber->isTerminated()) {
    $percentage = round($copied_count / $total_count, 2) * 100;
    writeToLog("[{$percentage}%]: Copied '{$copied[0]}' to '{$copied[1]}'");
    $copied = $fiber->resume();
    ++$copied_count;
}

writeToLog('Completed');

Output:

[33%]: Copied 'src/foo.png' to 'dest/foo.png'
[67%]: Copied 'src/bar.png' to 'dest/bar.png'
[100%]: Copied 'src/baz.png' to 'dest/baz.png'
Completed

The actual file copy operation is handled inside a Fiber, and the Fiber callback only accepts a list of files to copy, and their corresponding destination.

After a file is copied, the Fiber suspends it, and returns the source and destination names back to the caller. The caller then updates the progress, and logs information about the file that was just copied.

Using a while loop, the Fiber is resumed until it terminates. It is possible for the Fiber throw any Exception in case it cannot continue, and it will bubble up to the main program as well.

With the usage of Fiber, the callback stays lean because it does not need to handle other operations such as updating the progress.


Backwards Compatibility Impact

The Fiber class and its Throwables (FiberError/FiberExit) are new in PHP 8.1. Although the two Throwable can be poly-filled trivially, the Fiber class itself with its concurrency behavior cannot be brought to older PHP versions.

Any code that declares a class with name Fiber in the global namespace will cause fatal errors due to class re-declaration.


RFC Discussion Implementation