skip to content

In PHP, how does a Fiber differ from a Generator built with yield, and why can a Fiber pause from inside deeply nested function calls?

level: middleimportance: should knowfreq 28%

answer

  1. own stack versus one frame
  2. yield turns the function into a Generator
  3. yield from through every layer
  4. Fiber::suspend inside an array_map callback
  5. return types stay unchanged

basics

~20 s

A Generator pauses only at a yield in its own body, so every caller in between must also yield and return a Generator. A Fiber has its own call stack, so Fiber::suspend() pauses from any depth with callers unchanged.

solid answer

~40 s

Writing `yield` anywhere in a PHP function turns it into a generator function: calling it returns a `Generator` object, and only that function's own frame can pause. If a helper three calls down needs to pause, every function on the way must become a generator too and delegate with `yield from`, changing its return type. A `Fiber` instead gets its own call stack, so `Fiber::suspend()` can be called in any nested function, even inside a callback run by `array_map()` or a method called by `foreach` on an `Iterator`, and the whole stack pauses; the functions in between keep their normal signatures. The trade-off: generators are cheap and made for lazy iteration, while each fiber reserves a native stack (`fiber.stack_size`) and exists for libraries that must pause arbitrary code, such as async I/O.

code

php · 18 lines
php
<?php
declare(strict_types=1);

// The helper pauses; nothing above it changes signature.
function readInput(): string
{
    return Fiber::suspend('need-input');
}

function shout(): string
{
    return strtoupper(readInput());
}

$fiber = new Fiber(fn (): string => shout());
echo $fiber->start(), "\n";     // need-input
$fiber->resume('hi');
echo $fiber->getReturn(), "\n"; // HI

go deeper

for a junior

Recall that yield makes a function return a Generator, while a Fiber is created with new Fiber and paused with the static Fiber::suspend().

for a middle

Explain the one-frame versus whole-stack difference and show how it forces yield from through every caller but leaves fiber callers unchanged.

for a senior

Choose deliberately: generators for lazy data pipelines, fibers only through an async library, and weigh the per-fiber native stack set by fiber.stack_size.

for a principal

Judge how far async should reach into a code base; fibers keep signatures clean, but they also hide where code can pause, which reviewers can no longer see.

## Two ways PHP code can pause PHP has two built-in mechanisms that let a function stop and continue later: **generators** (since PHP 5.5, the `yield` keyword) and **fibers** (since PHP 8.1, the `Fiber` class). Both pause and resume, both pass values in and out, and both can have exceptions thrown into them. They differ in *what* is paused, and that decides what each is good for. ## How a Generator pauses Any function whose body contains `yield` becomes a **generator function**. Calling it runs nothing; it returns a `Generator` object, which implements `Iterator`. Each `next()`, `send()` or `foreach` step runs the body until the next `yield`. The key limit: **only the generator's own frame is paused**. A normal function it calls cannot yield on its behalf. If a helper deep in the call chain needs to pause, the pause must be passed up by hand: - the helper must contain `yield`, so it becomes a generator function too; - its caller must use `yield from helper()` to delegate, so it becomes one as well; - every function up the chain changes its return type to `Generator` (or `iterable`), and every call site changes. This spreading of `yield` through a code base is the price of a pause that lives in a single frame. ## How a Fiber pauses A `Fiber` runs its callable on a **separate call stack**. When any code on that stack calls the static `Fiber::suspend()`, the engine sets the whole stack aside: every function frame from the fiber's callable down to the suspension point stays intact. On `resume()` the stack is switched back in and the `suspend()` call simply returns. What is set aside includes the native stack of any internal function still in progress, such as `array_map()` halfway through its array, which is exactly what a generator has no way to preserve. The PHP manual stresses two consequences: 1. A function that calls `Fiber::suspend()` **does not change its return type**; its callers do not know it can pause. 2. A fiber can suspend inside functions called **by the engine**, such as a callback passed to `array_map()` or the `current()` method `foreach` calls on an `Iterator`. A generator cannot yield across such a call. That is why async libraries use fibers: a database or HTTP client can pause inside its read method, and your controller, repository and service code in between stay plain synchronous-looking PHP. ## Side by side | Aspect | `Generator` (`yield`) | `Fiber` | |---|---|---| | Created by | calling a function containing `yield` | `new Fiber(callable)` | | First run | `current()`, `next()`, `send()` or `foreach` | `start(...$args)` | | Pauses at | a `yield` in its own body only | `Fiber::suspend()` at any depth | | Value out | the yielded value (`current()`) | argument of `Fiber::suspend()`, returned by `start()`/`resume()` | | Value in | `send($v)` | `resume($v)` | | Exception in | `throw($e)` | `throw($e)` | | Result | `getReturn()` | `getReturn()` | | Iterable | yes, implements `Iterator` | no | | Memory | one frame | a native stack per fiber (`fiber.stack_size`) | ## Choosing between them - Use a **generator** for **lazy sequences**: reading a large file line by line, paging through query results, building a pipeline of transformations. It is iteration with a pause built in, and it is cheap. - Use a **fiber**, or more often a library that creates fibers for you, when **arbitrary code must pause** without being rewritten: async I/O with Revolt, Amp or ReactPHP's async package. - They combine: a generator can run inside a fiber, and a fiber may suspend while a generator's frame is on its stack. Fibers are not free. Each one reserves a native stack whose size is the `fiber.stack_size` ini setting; the engine defaults it to 2 MiB on 64-bit builds (1 MiB where pointers are smaller than 8 bytes). Thousands of idle generators cost little; thousands of fibers reserve that stack each. ## The interview point The expected answer is not "fibers are better generators". It is: a generator pauses **one frame** and so forces every caller to cooperate, while a fiber pauses **a whole stack** and so hides the pause from callers. Everything else, from the `yield from` chains to the unchanged return types, follows from that one difference.

  • Why can't a generator simply yield from inside a callback passed to array_map()?
    Putting `yield` in the callback makes the callback itself a generator function. `array_map()` would call it, receive a `Generator` object as the element value and move on; the outer generator does not pause at all. The pause belongs to the frame that contains `yield`, and `array_map()` does not iterate what it gets back.
  • When would you still choose a generator over a fiber?
    For lazy iteration: streaming a large file, paging a result set or chaining transformations with `foreach`. A generator is an `Iterator`, costs one frame rather than a native stack, and the `yield` points make the pause visible to readers. Fibers earn their cost only when unmodified code must pause, which in practice means an async I/O library.

saying these in an interview costs you the question

  • A Fiber is just a generator with a nicer API
  • Functions that call Fiber::suspend() must return a Generator
  • A generator can yield from any function it calls
  • Fibers are cheaper than generators because they need no object
  • A Fiber can be iterated with foreach like a generator