PHP 8.1: Fibers
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 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
Fiber::start
Fiber::suspend
Fiber::resume
Fiber::getCurrent
Fiber::getReturn
Fiber::throw
Fiber::isStarted
Fiber::isSuspended
Fiber::isRunning
Fiber::isTerminated
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 throw
s 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. CallingFiber::throw
on a running Fiber causes the same error as callingFiber::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
- Started Fibers include suspended, running, and terminated.
- Suspended Fibers is considered started, but not running, or terminated.
- Running Fibers are started, but not terminated or suspended.
- Terminated Fibers are started, but not running, or suspended.
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.