skip to content

Concurrency Options

PHP has no shared-memory threads in core, so it gets concurrency from Fibers under an event loop or from long-running workers. Interviewers probe what each model does and does not parallelise.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

10

In PHP, if you start two Fibers that each run a CPU-heavy loop or call sleep(), do they run in parallel, and why?

level: middleimportance: must knowfreq 38%

answer

  1. one thread, one fiber at a time
  2. switches only at explicit calls
  3. no time slicing in the engine
  4. sleep() stalls every fiber
  5. concurrency needs non-blocking I/O plus a loop

basics

~20 s

No. All fibers in a PHP process share one thread and only one runs at a time; control moves only when code calls Fiber::suspend(), start(), resume() or throw(). A CPU loop or sleep() inside one fiber stalls all the others.

solid answer

~40 s

No. Fibers give PHP **cooperative concurrency, not parallelism**. Every fiber in a process runs on the same thread, and exactly one of them, or the main script, executes at any instant. The engine never time-slices between fibers: a switch happens only when running code calls `Fiber::suspend()`, when the caller calls `start()`, `resume()` or `throw()`, or when a fiber finishes. So a CPU-bound loop runs to completion before anything else gets a turn, and a blocking call such as `sleep(1)`, a PDO query or `file_get_contents()` blocks the whole process, every fiber included. Two fibers each sleeping one second take two seconds. Fibers help only when a library turns waiting into suspension, using non-blocking streams and an event loop; CPU parallelism still needs more processes.

code

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

$t0 = hrtime(true);

$fibers = [];
foreach (['a', 'b'] as $name) {
    $fibers[] = new Fiber(function () use ($name): void {
        sleep(1);                 // blocks the whole process
        echo "$name woke\n";
    });
}

foreach ($fibers as $fiber) {
    $fiber->start();              // runs a to completion, then b
}

printf("elapsed %.1f s\n", (hrtime(true) - $t0) / 1e9); // ~2.0, not ~1.0

go deeper

for a junior

Remember the headline: PHP fibers are not threads, only one runs at a time, and sleep() inside a fiber pauses everything.

for a middle

Explain the explicit switch points, the absence of preemption, and why only suspension-aware I/O lets other fibers run during a wait.

for a senior

Spot the production traps: a stray PDO call or CPU burst inside a fiber-based service stalls every connection, and CPU scaling must come from more processes.

for a principal

Decide whether an async fiber stack fits a workload at all; I/O fan-out gains, CPU-bound work does not, and the non-blocking client ecosystem constrains the choice.

## The short answer Fibers do **not** run in parallel. They are a way to **interleave** several tasks on a single thread, and the interleaving happens only where the code asks for it. Interviewers ask this because the word "fiber" is often heard as "lightweight thread", and the thread half of that picture is wrong for PHP. ## Why only one fiber runs at a time A PHP process (a CLI script, one PHP-FPM worker, one worker of a long-running server) executes PHP code on one thread. A `Fiber` adds a **separate call stack**, not a separate thread. Switching fibers means the engine saves one stack and swaps another in on that same thread. The switch points are all explicit calls: - `$fiber->start()`, `$fiber->resume()` and `$fiber->throw()` switch **into** a fiber; - the static `Fiber::suspend()` switches **out of** the running fiber, back to whoever started or resumed it; - a fiber that returns or throws also hands control back to that caller. There is **no preemption and no time slicing**: the engine has no timer that interrupts a busy fiber to let another one run. PHP ships only the switching primitive; there is no built-in scheduler that decides which fiber goes next. (Since PHP 8.4 the engine may run destructors triggered by the garbage collector in a fiber of its own, but it never hands the thread to another of your fibers.) ## What that means for CPU work and blocking calls | Work inside a fiber | What the other fibers experience | |---|---| | A tight CPU loop with no suspension | nothing runs until the loop ends | | `sleep()` or `usleep()` | the whole process sleeps | | A PDO or mysqli query | the whole process waits on the database | | `file_get_contents()` on a URL | the whole process waits on the network | | A library call that suspends while waiting | the others run while this one waits | The last row is the only case where fibers overlap anything, and what overlaps is **waiting**, not computing. ## Where the concurrency actually comes from Fibers become useful for I/O through event-loop libraries such as **Revolt** (which Amp is built on) and **ReactPHP**'s async package. Roughly: 1. The library sets its sockets and streams to non-blocking mode. 2. When a read or write would wait, the library registers interest with its event loop and calls `Fiber::suspend()` for the current fiber. 3. The loop waits on all registered streams at once and resumes each fiber when its stream is ready. The program still does one thing at a time; it just stops wasting the waits. Ten HTTP calls that each wait 200 ms can finish in roughly 200 ms total, but ten 200 ms CPU computations still take two seconds. ## Reading a fiber snippet quickly When an interviewer shows fiber code and asks what runs when, trace it in four steps: 1. Find every `start()`, `resume()` and `throw()` call: control enters a fiber only there. 2. Inside each fiber, find every `Fiber::suspend()`, direct or inside a library call: control leaves only there. 3. Treat everything between those points as one uninterrupted block, including any `sleep()`, query or CPU loop it contains. 4. Add up the blocking blocks: their sum is the wall-clock time, however many fibers are involved. ## Getting real parallelism in PHP If the work is CPU-bound, the answer is **more processes**, not more fibers: - more PHP-FPM or worker processes, each handling its own requests; - a job queue with several consumer processes; - forking or spawning child processes from a CLI script. Each process has its own memory and runs on its own core, which is what parallelism requires. ## Common wrong answers - "Fibers are PHP's threads." They share one thread; only the stacks differ. - "Fibers make blocking functions non-blocking." `sleep()`, PDO and cURL's blocking calls behave exactly as before; only code written to suspend can yield the thread. - "The engine schedules fibers fairly." There is no engine scheduler; a fiber that never suspends keeps the thread. In an interview, say it in one line: **fibers let PHP overlap waiting, never computing**.

  • Does using Fibers require a thread-safe (ZTS) build of PHP?
    No. `Fiber` is part of the engine on every PHP 8.1+ build, thread-safe or not. A fiber creates no thread; it only allocates a separate stack and swaps it in on the thread the script already runs on, so there is nothing for thread safety to protect.
  • How would you speed up a PHP CLI job that resizes thousands of images?
    That work is CPU-bound, so fibers cannot help. Split the batch across several processes: a queue with multiple consumer workers, or a parent script that spawns child processes, each taking a slice of the images. Each process runs on its own core with its own memory, giving the parallelism fibers cannot.
  • Does a Fiber-based event-loop library make a PDO query non-blocking?
    No. PDO's calls block the thread until the database answers, and a blocked thread cannot run the loop or any other fiber. Fiber-friendly libraries ship their own database clients that speak the wire protocol over non-blocking sockets and suspend while waiting; PDO itself has no such mode.

A single cook working through several recipes: the cook switches recipes only when one says wait for the oven. Chopping still happens one board at a time, so more recipes never mean faster chopping.

saying these in an interview costs you the question

  • Two fibers doing CPU work finish twice as fast
  • Each Fiber runs on its own thread, so fibers use several cores
  • PHP's engine preempts a busy fiber so others get a turn
  • Starting code in a Fiber makes sleep() or PDO non-blocking
  • Fibers need a thread-safe (ZTS) PHP build
open as a page

After moving a PHP API to worker mode, some responses show another user's data; what state is leaking between requests, and how do you find and fix it?

level: seniorimportance: must knowfreq 35%

basics

~20 s

Request data, typically the authenticated user, was stored in something that outlives the request: a static property, a static function variable or a memoised singleton. The next request on that worker reuses it. Make such state request-scoped or reset it after each request.

open as a page

In PHP, what is a Fiber, and how do Fiber::start(), Fiber::suspend() and Fiber::resume() pass values between the fiber and its caller?

level: juniorimportance: should knowfreq 30%

basics

~20 s

A PHP Fiber (core since 8.1) runs a callable on its own call stack that can pause. start() runs it until Fiber::suspend($x), which makes start() return $x; resume($y) continues it and makes that suspend() call return $y.

open as a page

In PHP, which Fiber misuses throw FiberError, and what happens to an exception thrown inside a fiber or injected with Fiber::throw()?

level: middleimportance: should knowfreq 18%

basics

~10 s

FiberError, an Error subclass, marks wrong-state calls: starting twice, resuming a non-suspended fiber, Fiber::suspend() outside a fiber, an early getReturn(). Exceptions inside a fiber leave through the start(), resume() or throw() that entered it.

open as a page

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%

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.

open as a page

In PHP, what changes when an application runs in worker mode under FrankenPHP, RoadRunner or Swoole instead of one fresh request per script under PHP-FPM?

level: middleimportance: should knowfreq 32%

basics

~20 s

Worker mode boots the application once per worker and then loops, handling many requests in the same PHP process. Bootstrap cost disappears from each request, but the engine's end-of-request cleanup no longer runs between them, so statics, singletons and connections carry over.

open as a page

You are building a PHP chat server on an event-loop library such as Revolt, Amp or ReactPHP; how do Fibers let each connection handler call a plain-looking read while other connections keep being served?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Each connection's handler runs in its own fiber. When a read would wait, the library registers the socket with the event loop and suspends that fiber; the loop waits on every socket at once and resumes the fiber whose socket became readable.

open as a page

In a PHP worker-mode runtime, why do exit(), register_shutdown_function(), ini_set() and setlocale() behave differently than they do under PHP-FPM?

level: seniorimportance: should knowfreq 18%

basics

~20 s

These built-ins are tied to the end of a PHP request, and in worker mode the PHP request lasts the whole worker lifetime. exit ends the worker, shutdown functions run only when it exits, and ini_set() or setlocale() changes persist into later requests.

open as a page

A PHP service in worker mode grows memory with every request until workers die at memory_limit; how do you find the growth and contain it with worker recycling?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Log memory_get_usage() after each request: a plateau is warm caches, a steady climb is a leak, usually data appended to long-lived arrays. Fix the source, and recycle workers after N requests or above a memory threshold so growth never reaches memory_limit mid-request.

open as a page

Would you move a mature PHP-FPM application to a worker-mode runtime such as FrankenPHP, RoadRunner or Swoole, and how would you decide and de-risk the rollout?

level: principalimportance: nice to knowfreq 14%

basics

~20 s

Only when bootstrap is measurably a large share of request time and the code can be made reset-safe. Audit statics, singletons, exit and global settings, add two-user tests, then canary with conservative recycling and keep PHP-FPM as the fallback.

open as a page