A Look at the New PHP 8.1 Fibers Feature

31

March 2021

A Look at the New PHP 8.1 Fibers Feature

By: Tree Web Solutions | Tags: PHP 8.1, new php 8.1, php 8.1 fibers feature, how will fibers work

PHP is trying to shove the lack of features from its code base, and Fibers is one of the meaningful additions to the language. PHP Fibers, incoming in PHP 8.1 at the end of the year, will introduce a sort of async programming (coroutines) into the wild.

The fiber concept basically refers to a lightweight thread of execution (also called coroutine). These seem to run in parallel but are ultimately handled by the runtime itself rather than by pushing it directly to the CPU. A lot of major languages have their own ways to implement them, but the principle is the same: Let the computer do two or more things at the same time, and wait until everything is completed.

PHP implementation of Fibers is not true asynchronous computing, as one may think. Indeed, the core of PHP will be still synchronous after Fibers are introduced.

You can think of PHP Fibers as being like switching from one car to another.

A Fiber is a single final class that looks like a car: It can start and run immediately, push the brakes and wait, and resume its trip.

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 {}
}

When you create a new Fiber instance with a callable, nothing will happen. Is not until you start the Fiber that the callback is executed like any other normal PHP code.

$fiber = new Fiber(function() : void {
echo "I'm running a Fiber, yay!";
});
$fiber->start(); // I'm running a Fiber, yay!

Didn’t I say Fibers were asynchronous? They are, but only until you hit the brakes by calling Fiber::suspend() inside the callback. Then it passes the control to the “outside,” but bear in mind this Fiber car is still alive and waiting to resume.

$fiber = new Fiber(function() : void {
Fiber::suspend();
echo "I'm running a Fiber, yay!";
});
$fiber->start(); // [Nothing happens]

Now that the car is suspended, the next thing to do is to take your foot off the brake, and for that, we can call the resume() method from the outside.

$fiber = new Fiber(function() : void {
   Fiber::suspend();
   echo "I'm running a Fiber, yay!";
});
$fiber->start(); // [Nothing happened]
$fiber->resume(); // I'm running a Fiber, yay!

This is literally untrue async, but that doesn’t mean your application can’t do two things at a time. The real truth here is that the Fiber function state is saved where it was left off. You’re figuratively switching between cars, driving each to one point.

One of the neat things about start()suspend(, and resume() is that they accept arguments:

  • The start() method will pass the arguments to the callable and will return whatever the suspend() method receives.
  • The suspend() method returns whatever value the resume() method received.
  • The resume() method returns whatever the next call to suspend() received.

This makes communication between the Main thread and the Fiber relatively easy:

  • resume() is used to put values into the Fiber that are received with suspend() , and
  • suspend() is used to push values out that are received by resume().

This makes the official example way easier to understand:

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('fiber');
    echo "Value used to resume fiber: ", $value, "\n";
});
 
$value = $fiber->start();
 
echo "Value from fiber suspending: ", $value, "\n";
 
$fiber->resume('test');

If you execute the above code, you will receive something like this:

Value from fiber suspending: fiber
Value used to resume fiber: test

Let’s face it, PHP is paired with nginx/Apache 99% of the time, mainly because it is not multithreaded. The server that comes in PHP is blocking and serves only for some tests or showing something to a client.

Fibers may open the door to letting PHP work with the socket more efficiently, and enable things like WebSockets, server-side events, pooled database connections, or even HTTP/3, without having to resort to compiling extensions, hacking your way down with unintended features, encapsulating PHP into another external runtime, or any other recipe for disaster.

Some things may take time to settle, but if there is a promise of keeping a single code base for other features, without having to spend days trying to compile and deploy, I’m on board.

According to the documentation, Fibers offers “only the bare minimum required to allow user code to implement full-stack coroutines or green-threads in PHP.”

In other words, unless you have a very weird reason to use them directly, you will never have to interact with Fibers like you would doing coroutines on Javascript or Go.

Some high-level frameworks (like SymfonyLaravelCodeIgniter, and CakePHP, among others) will take some time to understand how to approach Fibers and create a set of tools for them to work with from a developer standpoint. Some low-level frameworks, like amphp and ReactPHP, have already boarded the Fiber ship in their latest development versions.

While this will free you from thinking more about Fibers rather than your idea, it means that everyone will make their own flavor of concurrency, with all their advantages and caveats.

I’m going to quote Aaron Piotrowski from PHP Internals Podcast #74: “Since only one Fiber can be executing at the same time, you don’t have some of the same race conditions that you have with memory being accessed or written to by two threads at the same time.”

Aaron also adds that it will be the frameworks that will be able to tackle the concurrency and synchronization problem over the same piece of memory.

This is good because you won’t need to think about data races, semaphores, and mutexes, things that gophers understand perfectly. But you’re still bound to essentially only two things at a time, no matter what you do.

Since only one Fiber is running at the same time, even if you declare multiple ones, there is no problem of synchronization of data. But Aaron said that there is the potential of another Fiber waking up and rewriting what the first Fiber is sharing. One of the solutions is to use Go’s channels style.

Derick Rethans asked about channels, and Aaron’s answer is simple: Something else must be implemented alongside Fibers, but until then, the frameworks will have the last word on how to use channels, if they deem them necessary for what they offer, like the guys at amphp.

The Go language has been gaining traction a lot of these months, especially thanks to being built around concurrency from the ground up. Anything can be executed concurrently with the go keyword, and synchronization is done by mutexes or channels, making it brain-dead easy to work with.

names := make(chan string)go doFoo(names)
go doBar(names)

From that perspective, Go is miles ahead of PHP’s initial concurrency solution. If you need something fully multithreaded, you may want to make your software in Go, or even Rust if you want to use CPU threads directly.

It’s not that PHP is not compatible with any concurrency model, but surely its foundation is still synchronous at its core, at the expense of convenience and better understandability. Compared to Go, the latter suffers from excessive plumbing.

Source: https://betterprogramming.pub

Share this Post