Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

Async & Coroutines

Run I/O in parallel. One API, two functions, 3x speedup.

You will learn

  • Why sequential I/O wastes time
  • The go() + Channel pattern for parallel work
  • When coroutines help (I/O) and when they don't (CPU)
  • Task workers for CPU-bound jobs

The problem

Your app needs to call two APIs before rendering a page. Sequentially: 500ms + 300ms = 800ms. But the two calls don't depend on each other. What if you could run them at the same time and wait only for the slower one?

The mental model

Coroutines are like juggling. You throw ball 1 (API call 1) and while it's in the air, you throw ball 2 (API call 2). You catch them as they come down. You never waited idle — while one ball was airborne, your hands were busy throwing the next.

go() is throwing a ball. $ch->pop() is catching it.

The pattern: go() + Channel

use OpenSwoole\Coroutine\Channel;

$app->route('/dashboard', function() {
    $ch = new Channel(2);

    go(function() use ($ch) {
        co::sleep(0.5);                // simulate 500ms API call
        $ch->push(['users' => 42]);
    });

    go(function() use ($ch) {
        co::sleep(0.3);                // simulate 300ms DB query
        $ch->push(['posts' => 128]);
    });

    $results = [];
    for ($i = 0; $i < 2; $i++) {
        $results[] = $ch->pop();       // blocks THIS coroutine, not the worker
    }
    return $results;
    // Total: ~500ms (max), not 800ms (sum)
});

go() spawns a new coroutine on the same worker. $ch->pop() suspends the parent coroutine until a value arrives — but the worker thread is free to handle other requests while it waits.

Live demo: sequential vs parallel

The endpoint below runs three 100ms sleeps. Sequential: ~300ms. Parallel via go() + Channel: ~100ms.

Timing comparison

co::sleep() vs usleep()

co::sleep(0.5);  // yields — other coroutines run while this one sleeps
usleep(500000);  // blocks — the worker thread is stuck for 500ms

Always use co::sleep() inside coroutine contexts. The one exception: inside a Generator returned from a route handler, co::sleep() is a no-op — use usleep() for artificial delays there.

🔎 Task workers for CPU-bound jobs

ZealPHP supports task workers for background jobs (sending emails, generating PDFs, crunching data). Dispatch via App::getServer()->task(['handler' => '/task/job', 'args' => [...]]). Task handlers live in task/. They run in separate processes, so they won't block request workers.

Key Takeaways

  • go() spawns a coroutine; Channel synchronizes results
  • Parallel I/O: total time = max(tasks), not sum(tasks)
  • Coroutines help with I/O (network, file, DB) — not CPU computation
  • Use task workers for CPU-heavy work that would block request handling