Concurrency¶
Kinetis\Async is a thin wrapper over Revolt — the
AMPHP v3 event loop — not a hand-rolled reactor. PHP Fibers alone are just
cooperative coroutines; they need an event loop scheduling I/O around them,
and Revolt is the PHP ecosystem’s de facto standard for that. Nothing in
this page is Kinetis reinventing an event loop — it’s a small, deliberately
thin layer of primitives built on Revolt’s suspend/resume pattern.
The suspend/resume pattern¶
Every non-blocking primitive in Kinetis\Async is built the same way:
capture the currently-running Fiber, register a Revolt watcher for
whatever condition you’re waiting on, then suspend — and let the watcher’s
callback resume the fiber once that condition is met.
use Revolt\EventLoop;
$fiber = Fiber::getCurrent();
EventLoop::onReadable($stream, static function (string $watcherId) use ($fiber): void {
EventLoop::cancel($watcherId);
$fiber?->resume();
});
Fiber::suspend();
Timer::delay() is the simplest concrete example — a Fiber-suspending
delay with nothing to read or write, useful mainly as a deterministic way
to prove concurrency actually overlaps, without depending on real network
timing in a test:
use Kinetis\Async\Timer;
Timer::delay(0.05); // suspends this Fiber for 50ms, without blocking the process
Socket — non-blocking TCP¶
use Kinetis\Async\Socket;
$socket = Socket::connect('example.com', 80);
$socket->write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
$response = $socket->read(4096);
connect()/read()/write() suspend the calling Fiber while the
underlying stream isn’t ready, rather than blocking on it — the worker is
free to run other Fibers, or process other watchers, for however long that
takes. Calling any of these methods outside a Fiber is a programming
error — there’s nothing to resume — and surfaces as PHP’s own
FiberError, not a silent hang.
Warning
This is why Kinetis’s database clients (see Persistence) aren’t
built on PDO, ext-mysqli, or ext-pgsql. A blocking call has no point
where it can hand control back to other work, so wrapping one in a Fiber
doesn’t make it non-blocking — it blocks the entire worker process just
as hard, only less visibly, defeating concurrently()’s whole purpose.
concurrently() — running tasks side by side¶
use function Kinetis\Async\concurrently;
[$user, $orders, $inventory] = concurrently([
fn () => $userClient->fetch($userId),
fn () => $ordersClient->fetch($userId),
fn () => $inventoryClient->checkStock($sku),
]);
Each task becomes its own Fiber. A single EventLoop::run() call
afterwards drives all of them to completion — Revolt’s own run loop
doesn’t return until nothing is left to wait on, so it doesn’t matter how
many times any one task suspends internally, or in what order they finish.
Every task runs to completion — successfully or not — before any
exception is allowed to surface. A failing task doesn’t abort the others
still in flight; if one or more failed, the first failure (in $tasks
order) is rethrown only once everything has finished:
try {
concurrently([
fn () => $slowButSuccessful(),
fn () => throw new RuntimeException('this one fails'),
]);
} catch (RuntimeException $e) {
// $slowButSuccessful() still ran to completion — it just wasn't
// allowed to abort the other task's own error from surfacing.
}
Three 50ms Timer::delay() calls run through concurrently() complete in
well under 100ms total, not the ~150ms+ a sequential fallback would take —
because they genuinely overlap rather than merely appearing to.
Composing with AMPHP¶
Kinetis\Async’s database and Redis clients (see Persistence) come
from the AMPHP ecosystem — amphp/mysql, amphp/postgres, amphp/redis —
chosen specifically because they’re Revolt-native, needing no bridging
between two different event loops. They use Amp\Future/coroutines
internally, a different API shape than Kinetis\Async’s own raw
Fiber::suspend()/EventLoop::onX() primitives, but both run on the same
underlying Revolt loop, so a concurrently() call can freely mix tasks
built on either shape and still run every one of them genuinely in
parallel: a MySQL query, a Postgres query, and a Redis command issued
together complete in roughly the time the slowest one alone takes, not the
sum of all three.
See also¶
Persistence — the AMPHP-based database/Redis clients described above, and
TransactionGuard, the request-lifecycle safety net built specifically because AMPHP’s connection pools have no concept of Kinetis’sRequestScope.