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.
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;Channelsynchronizes 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