0

Coroutines vs Fibers vs Generators — conceptual differences

Expert5 min read·php-11-006
interviewcompare

Concept

Three terms — coroutines, Fibers, and generators — are often used interchangeably by PHP developers, but they represent distinct abstractions with important differences in capability, direction of data flow, and scheduling control.

Generators (PHP 5.5+, enhanced in PHP 7) are the simplest: they are single-directional resumable functions. A generator function uses yield to produce a sequence of values lazily. The caller drives the generator via foreach or explicit next()/current() calls. Generators support bidirectional communication via send($value) (which becomes the result of yield inside the generator) and throw(). However, generators cannot suspend themselves — they rely on the caller to drive them. They cannot yield from inside a called function (only from directly within the generator body, or via yield from). They are fundamentally pull-based iterators.

Fibers (PHP 8.1) are symmetric coroutines: the Fiber can suspend itself at any call depth via the static Fiber::suspend() method, even from deeply nested function calls. This is the key distinction — a generator can only yield at the top level of the generator function, but a Fiber can suspend from arbitrarily deep in its call stack. Fibers are the correct primitive for building event loop schedulers.

Coroutines is a broader computer science term. Both generators (used with send()) and Fibers are coroutines — routines that can cooperatively suspend and resume. The distinction is symmetric vs asymmetric: generators are asymmetric coroutines (caller has all the control), while Fibers are symmetric coroutines (the Fiber itself decides when to suspend). ReactPHP's documentation uses "coroutine" to refer to generators used with Amp v2's older Coroutine class; Amp v3 uses Fibers.

Go goroutines are neither: they are lightweight threads scheduled preemptively by Go's runtime across OS threads with a work-stealing scheduler. They can truly run in parallel on multiple cores. PHP has no equivalent — the closest analogy is Swoole's coroutines, which are cooperative (not preemptive) but benefit from Swoole's multi-worker architecture for parallelism.

FeatureGeneratorFiberGo goroutine
Suspend from nested callsNoYesYes (preemptive)
Bidirectional dataYes (send/yield)Yes (suspend/resume)Channel-based
True parallelismNoNoYes
SchedulingCaller-drivenScheduler-drivenRuntime-driven
PHP version5.5+8.1+N/A

Code Example

php
<?php
declare(strict_types=1);

// -------------------------------------------------------
// 1. Generator as an asymmetric coroutine (bidirectional)
// -------------------------------------------------------

function logProcessor(): \Generator
{
    $processed = 0;
    while (true) {
        // yield receives a value FROM the caller via send().
        $line = yield $processed; // also yields $processed back to caller

        if ($line === null) {
            return $processed; // Generator::getReturn() after null send
        }

        // Simulate processing: count ERROR lines.
        if (str_contains($line, 'ERROR')) {
            $processed++;
        }
    }
}

$gen = logProcessor();
$gen->current(); // Prime the generator (advance to first yield).

$gen->send('[INFO] Server started');
$gen->send('[ERROR] Database timeout');
$gen->send('[ERROR] Queue connection refused');
$errorCount = $gen->send(null); // Sends null → generator returns

echo "Errors processed: {$errorCount}\n"; // 2

// -------------------------------------------------------
// 2. Fiber — suspend from deeply nested call
// -------------------------------------------------------

function deeplyNested(): string
{
    // This is called from within a Fiber.
    // We can suspend from HERE — not possible with a generator.
    \Fiber::suspend('waiting for DB');
    return 'DB result';
}

function queryLayer(): string
{
    return deeplyNested(); // Nested call — Fiber can still suspend here.
}

$fiber = new \Fiber(function (): void {
    $result = queryLayer(); // Two levels deep.
    echo "Got: {$result}\n";
});

$suspended = $fiber->start();   // Returns 'waiting for DB'
echo "Fiber suspended with: {$suspended}\n";
$fiber->resume();               // Continues from inside deeplyNested()

// -------------------------------------------------------
// 3. Generator cannot suspend from a nested call
// -------------------------------------------------------

function notAGenerator(): string
{
    // yield 'hello'; // This is a SYNTAX ERROR — yield is only valid
                      // directly inside the generator function body.
    return 'hello';   // We can only return from here.
}

function generatorThatCallsOut(): \Generator
{
    $value = notAGenerator(); // Calling this does NOT suspend the generator.
    yield $value;             // Must yield here, at the generator's own level.
}

foreach (generatorThatCallsOut() as $v) {
    echo $v . "\n"; // "hello"
}

Interview Q&A

Q: Why were Fibers added to PHP 8.1 if generators already supported bidirectional communication via send()?

Generators can only yield at the top level of the generator function — you cannot yield from inside a function called by the generator. This means that for a generator to be useful in an event loop, every I/O function it calls must itself be a generator and must propagate yields back up through the entire call chain with yield from. This is the "function color" problem: you must mark every function in the call chain as a generator. Amp v2 used this pattern extensively and it produced deeply nested yield from chains. Fibers solve this by making suspend a static method callable from any depth in the call stack. I/O functions do not need to be generators — they just call Fiber::suspend(). This made Amp v3 and other modern async PHP libraries dramatically simpler to write and use.


Q: Can you use generators inside Fibers or vice versa?

Yes, both directions work and compose naturally. A Fiber's callable can contain generator functions — you iterate them normally with foreach or yield from. A generator can create and start Fibers inside its body. What you cannot do is yield from inside a Fiber (there is no generator context), and you cannot Fiber::suspend() from inside a generator that is not running inside a Fiber (there is no Fiber context). The common real-world pattern in Amp v3 is: the event loop runs as a Fiber, which calls application code that uses Amp's async functions (which internally do Fiber::suspend()), while the application code itself may use generators for lazy iteration.


Q: How do Go goroutines differ from PHP Fibers at the scheduler level?

Go's runtime implements an M:N threading model — it maps M goroutines onto N OS threads, where N equals GOMAXPROCS (typically the number of CPU cores). Goroutines are scheduled preemptively: the Go runtime can interrupt a goroutine at any point (safepoints) and move it to a different OS thread. This enables true parallel execution of CPU-bound goroutines. PHP Fibers are 1:1 with the main OS thread — only one Fiber runs at a time, switches happen only at explicit Fiber::suspend() calls (cooperative, not preemptive), and there is no parallel execution. PHP Fibers are closer to single-threaded Python asyncio coroutines than to Go goroutines.