0

Concurrency vs parallelism — the PHP model

Intermediate5 min read·php-11-001
interviewcompare

Concept

Concurrency and parallelism are two distinct concepts that are routinely conflated, and PHP's architecture makes that distinction especially important. Parallelism means multiple computations executing at the exact same instant on separate CPU cores or processes. Concurrency means structuring a program so that multiple tasks can be in progress simultaneously, even if they are not running at the same moment — they interleave by switching context when one blocks on I/O.

Traditional PHP-FPM is neither concurrent nor parallel within a single process. Each request gets its own OS process or thread (managed by FPM's process pool), runs to completion, and dies. Horizontal scaling happens at the process level, not within a single PHP runtime. This share-nothing model is safe but wastes CPU time whenever a request blocks on a database query, an HTTP call, or a file read — the OS process just sleeps.

PHP 8.1 introduced Fibers, which bring intra-process concurrency to PHP. A Fiber is a cooperative, user-space coroutine: it runs until it voluntarily suspends via Fiber::suspend(), yields control back to the scheduler (your code), and can later be resumed. Critically, Fibers do not run in parallel — only one Fiber executes at a time. Their value is in avoiding blocking: while Fiber A waits for a database result, the scheduler can run Fiber B. This is the same model as JavaScript's event loop and Python's asyncio.

True parallelism in PHP requires multiple OS processes. The pcntl_fork() function (POSIX systems only, never available in FPM) creates a child process that is an exact copy of the parent. Extensions like Swoole and OpenSwoole implement their own coroutine scheduler built on top of an epoll/kqueue event loop, enabling thousands of concurrent connections in a single PHP process. Laravel Octane leverages these runtimes to keep the application bootstrapped between requests.

The practical takeaway for senior engineers: use Fibers (or ReactPHP/Amp coroutines) when your bottleneck is I/O latency and you want to multiplex work in a single process. Use pcntl_fork() or job queues when your bottleneck is CPU and you need true parallelism across cores.

ModelConcurrentParallelPHP mechanism
Traditional FPMNo (per-process)Yes (many processes)OS process pool
Fibers / Amp / ReactPHPYesNoUser-space scheduler
pcntl_forkYesYesOS fork
Swoole coroutinesYesYes (multi-worker)Extension event loop

Code Example

php
<?php
declare(strict_types=1);

/**
 * Demonstrates the difference between sequential blocking
 * and concurrent cooperative execution using Fibers.
 *
 * This is NOT production code — it simulates I/O with usleep()
 * to make the scheduling visible without real sockets.
 */

/**
 * A minimal cooperative scheduler that runs Fibers round-robin.
 * A real scheduler (ReactPHP, Amp) would use stream_select()
 * to wake Fibers only when their socket is readable.
 */
final class Scheduler
{
    /** @var \Fiber[] */
    private array $ready = [];

    public function add(\Fiber $fiber): void
    {
        $this->ready[] = $fiber;
    }

    public function run(): void
    {
        while ($this->ready !== []) {
            $fiber = array_shift($this->ready);

            if (!$fiber->isStarted()) {
                $fiber->start();
            } elseif ($fiber->isSuspended()) {
                $fiber->resume();
            }

            // If still suspended after resume, keep it in the queue.
            if ($fiber->isSuspended()) {
                $this->ready[] = $fiber;
            }
        }
    }
}

// Simulate two concurrent "HTTP fetch" tasks.
$fetchA = new \Fiber(function (): void {
    echo "Fiber A: starting request\n";
    // Simulate network latency — in real code this would be a non-blocking socket.
    \Fiber::suspend();
    echo "Fiber A: response received\n";
    \Fiber::suspend();
    echo "Fiber A: processing done\n";
});

$fetchB = new \Fiber(function (): void {
    echo "Fiber B: starting request\n";
    \Fiber::suspend();
    echo "Fiber B: response received\n";
    \Fiber::suspend();
    echo "Fiber B: processing done\n";
});

$scheduler = new Scheduler();
$scheduler->add($fetchA);
$scheduler->add($fetchB);
$scheduler->run();

/*
Output:
  Fiber A: starting request
  Fiber B: starting request
  Fiber A: response received
  Fiber B: response received
  Fiber A: processing done
  Fiber B: processing done

Both fibers made progress interleaved — concurrent, not parallel.
*/

Interview Q&A

Q: PHP-FPM already scales horizontally by spawning many worker processes. What concrete problem do Fibers solve that FPM cannot?

FPM's worker count is bounded by server memory — a typical 256 MB worker means maybe 100–200 workers per server. If each worker spends 80 ms of a 100 ms request waiting for a database query, 80% of that process's lifetime is idle. Fibers let a single PHP process multiplex many in-flight I/O operations: while one Fiber waits on a DB result, another can be preparing an HTTP response. This is meaningful in microservice architectures where a single page aggregates 10–15 upstream API calls. With Fibers those calls can be issued concurrently, reducing total wall-clock time from sum(latencies) to max(latencies).


Q: Can two Fibers run truly in parallel (simultaneously on two CPU cores)?

No. PHP's core runtime is single-threaded (it does not use OS threads for user code). Only one Fiber is executing at any instant. Fibers are a cooperative multitasking primitive — they switch context only at explicit Fiber::suspend() calls, not preemptively. True CPU parallelism in PHP requires separate OS processes (pcntl_fork, process pools via Swoole workers, or job queue workers). The Parallel extension (ext-parallel) does add real OS threads but is experimental and rarely used in application code.


Q: What is the difference between PHP's Fibers and JavaScript's async/await?

Structurally they are the same model — both are cooperative coroutines on a single-threaded runtime driven by an event loop. The key difference is ergonomics: JavaScript's async/await is built into the language and every I/O API in Node.js returns a Promise that the runtime resolves when the event loop fires. In PHP, the language provides only the raw Fiber primitive. You must supply your own scheduler and I/O multiplexer (via ReactPHP, Amp, or Swoole) and any library that does blocking I/O (MySQLi, cURL in synchronous mode) will still block the entire process. Fibers alone do not make PHP async — the I/O layer must also be non-blocking.