PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PHP Fibers deep dive – own scheduler, cooperative multitasking, parallel HTTP requests

by Henryk Tews / Tuesday, 15 July 2025 / Published in PHP

PHP 8.1 introduced Fibers as a foundation for cooperative multitasking. Most developers use them only indirectly through ReactPHP or Amp. But understanding Fibers at the mechanism level unlocks the ability to build your own concurrency primitives. I show how Fibers work, implement a minimal cooperative scheduler, and use it to run parallel HTTP requests without a framework dependency.

What is a Fiber?

<?php

declare(strict_types=1);

// A Fiber is a lightweight coroutine - it can be suspended and resumed
// Unlike threads, only one Fiber runs at a time (cooperative, not preemptive)

$fiber = new Fiber(function(): string {
    echo "Fiber: step 1\n";
    $received = Fiber::suspend('from fiber: first pause'); // pause here
    echo "Fiber: resumed with: {$received}\n";
    Fiber::suspend('from fiber: second pause');
    echo "Fiber: finishing\n";
    return 'fiber result';
});

// Start the fiber - runs until first Fiber::suspend()
$value1 = $fiber->start();
echo "Main: fiber paused, got: {$value1}\n"; // 'from fiber: first pause'

// Resume the fiber - runs until next Fiber::suspend() or return
$value2 = $fiber->resume('hello from main');
echo "Main: fiber paused again, got: {$value2}\n"; // 'from fiber: second pause'

// Resume to completion
$fiber->resume('final resume');
echo "Main: fiber terminated, returned: " . $fiber->getReturn() . "\n";

// Output:
// Fiber: step 1
// Main: fiber paused, got: from fiber: first pause
// Fiber: resumed with: hello from main
// Main: fiber paused again, got: from fiber: second pause
// Fiber: finishing
// Main: fiber terminated, returned: fiber result

Building a minimal cooperative scheduler

<?php

declare(strict_types=1);

class CooperativeScheduler
{
    /** @var Fiber[] */
    private array $queue = [];
    private array $callbacks = []; // fiber => callback when done

    // Add a task to the scheduler
    public function add(callable $task, ?callable $onDone = null): void
    {
        $fiber = new Fiber($task);
        $this->queue[] = $fiber;
        if ($onDone !== null) {
            $this->callbacks[spl_object_id($fiber)] = $onDone;
        }
    }

    // Run until all tasks complete
    public function run(): void
    {
        // Start all fibers
        $started = [];
        foreach ($this->queue as $fiber) {
            $fiber->start();
            if (!$fiber->isTerminated()) {
                $started[] = $fiber;
            } elseif ($cb = $this->callbacks[spl_object_id($fiber)] ?? null) {
                $cb($fiber->getReturn());
            }
        }
        $this->queue = $started;

        // Round-robin until all finish
        while (!empty($this->queue)) {
            $stillRunning = [];
            foreach ($this->queue as $fiber) {
                if ($fiber->isSuspended()) {
                    $fiber->resume();
                }
                if (!$fiber->isTerminated()) {
                    $stillRunning[] = $fiber;
                } elseif ($cb = $this->callbacks[spl_object_id($fiber)] ?? null) {
                    $cb($fiber->getReturn());
                }
            }
            $this->queue = $stillRunning;
        }
    }
}

// Usage
$scheduler = new CooperativeScheduler();
$results   = [];

for ($i = 1; $i <= 5; $i++) {
    $scheduler->add(
        function() use ($i): string {
            echo "Task {$i}: starting\n";
            Fiber::suspend(); // yield control back to scheduler
            echo "Task {$i}: continuing\n";
            Fiber::suspend(); // yield again
            echo "Task {$i}: finishing\n";
            return "result_{$i}";
        },
        function(string $result) use (&$results): void {
            $results[] = $result;
        }
    );
}

$scheduler->run();
print_r($results); // ['result_1', 'result_2', ..., 'result_5']

Parallel HTTP requests with a socket-based scheduler

<?php

declare(strict_types=1);

// Non-blocking HTTP using sockets + Fibers
// Conceptually what ReactPHP does internally

class AsyncHttpClient
{
    private array $pendingSockets = [];

    public function fetchAsync(string $url): mixed
    {
        $parsed = parse_url($url);
        $host   = $parsed['host'];
        $path   = $parsed['path'] ?? '/';
        $port   = $parsed['port'] ?? ($parsed['scheme'] === 'https' ? 443 : 80);

        // Open non-blocking socket
        $socket = fsockopen(
            ($parsed['scheme'] === 'https' ? 'ssl://' : '') . $host,
            $port,
            $errno, $errstr, 10
        );
        stream_set_blocking($socket, false);

        // Send HTTP request
        $request = "GET {$path} HTTP/1.1\r\n"
            . "Host: {$host}\r\n"
            . "Connection: close\r\n\r\n";
        fwrite($socket, $request);

        // Register socket with current fiber
        $this->pendingSockets[spl_object_id(Fiber::getCurrent())] = $socket;

        // Suspend: yield control back to scheduler
        // Scheduler will resume us when socket has data
        return Fiber::suspend($socket);
    }

    public function readResponse($socket): string
    {
        $response = '';
        while (!feof($socket)) {
            $chunk = fread($socket, 8192);
            if ($chunk === false || $chunk === '') {
                Fiber::suspend($socket); // yield while waiting for more data
            } else {
                $response .= $chunk;
            }
        }
        fclose($socket);
        return $response;
    }
}

// Scheduler that watches sockets and resumes fibers when ready
class IoScheduler extends CooperativeScheduler
{
    private array $socketToFiber = [];

    public function run(): void
    {
        // Extended scheduler that checks sockets between fiber runs
        // Full implementation would use stream_select() for efficiency
        parent::run();
    }
}

Fibers in production – the practical summary

<?php

// In practice: use ReactPHP or Amp, not raw Fibers
// They provide the scheduler, I/O integration, and Promise/async patterns

// ReactPHP with Fibers (via react/async package)
// - This is the practical use case for Fibers in PHP today

require 'vendor/autoload.php';

use function React\Async\async;
use function React\Async\await;
use function React\Async\parallel;

// Fetch 10 API endpoints in parallel using Fibers under the hood
async(function() {
    $urls = array_map(
        fn($i) => "https://jsonplaceholder.typicode.com/posts/{$i}",
        range(1, 10)
    );

    $client = new \React\Http\Browser();

    // All 10 requests fire concurrently
    $tasks = array_map(fn($url) => async(function() use ($client, $url) {
        $response = await($client->get($url));
        return json_decode($response->getBody(), true);
    }), $urls);

    $results = await(parallel($tasks));
    echo count($results) . " posts loaded in parallel\n";
})();

\React\EventLoop\Loop::run();

Summary

Fibers are PHP’s cooperative concurrency primitive. A Fiber runs until it calls Fiber::suspend(), at which point control returns to whoever called start() or resume(). A scheduler sits on top and decides which Fiber to resume next. ReactPHP, Amp, and Laravel Octane’s async support all use Fibers under the hood – understanding the mechanism helps when debugging async code or building custom concurrency constructs for import pipelines and API fan-out operations.

About Henryk Tews

What you can read next

PHP 7.2 – object type hint, sodium instead of mcrypt, deprecations

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}