A minimal cooperative task scheduler for PHP, built on native fibers. Run several tasks on a single thread, hand control back and forth at points you choose, and wait for sub-tasks to finish — in a few dozen lines of dependency-free code.
FiberLoops is a tiny scheduler. You queue tasks with defer() and drive them
with run(). Each task is a fiber; the loop advances the tasks round-robin,
running each one until it cooperatively yields (next() / sleep()) or
returns.
Scheduling is cooperative, not preemptive: nothing interrupts a task. A task
keeps the CPU until it yields or finishes, so long-running tasks must call
next() periodically to let their siblings make progress. There is no I/O
reactor, no stream/timer polling, no threads — it is a scheduling primitive you
can build those things on top of, not a full async runtime like ReactPHP or Amp.
- PHP 8.1+ (fibers are a core language feature since 8.1 — no extension needed)
composer require initphp/fiber-loopsrequire_once 'vendor/autoload.php';
use InitPHP\FiberLoops\Loop;
$loop = new Loop();
$loop->defer(function () use ($loop) {
foreach (['a', 'b', 'c'] as $step) {
echo "task-1: $step\n";
$loop->next(); // yield: let other tasks run
}
});
$loop->defer(function () use ($loop) {
foreach (['x', 'y', 'z'] as $step) {
echo "task-2: $step\n";
$loop->next();
}
});
$loop->run(); // drive every task to completiontask-1: a
task-2: x
task-1: b
task-2: y
task-1: c
task-2: z
The two tasks interleave because each one yields with next() after every step.
Remove the next() calls and the first task would run to completion before the
second one started.
Loop implements InitPHP\FiberLoops\LoopInterface. Depend on the interface when
you want to substitute or mock the scheduler.
| Method | Description |
|---|---|
defer(callable|Fiber $task): void |
Queue a task. A callable is wrapped in a fiber. Safe to call during run(). |
run(): void |
Run every queued task to completion (blocks until the queue is empty). |
next(mixed $value = null): mixed |
Yield from the current task back to the scheduler. Must run inside a fiber. |
sleep(int|float $seconds): void |
Cooperatively pause the current task. Must run inside a fiber. |
await(callable|Fiber $task): mixed |
Run a task to completion and return its value, yielding while it works. |
defer() queues work; run() executes it. A task added during run() (from
inside another task) is picked up on the next scheduling pass, so tasks can spawn
more tasks.
next() is the yield point. Calling it suspends the current task and lets the
scheduler advance the others; the task resumes where it left off on the next pass.
It must be called from within a fiber (i.e. inside a deferred or awaited task) —
calling it from the main script throws a LoopException:
$loop->next(); // LoopException: Loop::next() must be called from within a fiber...The bundled scheduler resumes tasks without a value, so
next()returnsnullunderrun()andawait(). The$valueargument is reserved for custom drivers and is ignored here.
sleep() pauses the current task for at least the given number of seconds while
letting sibling tasks keep running:
$loop = new Loop();
$loop->defer(function () use ($loop) {
$loop->sleep(0.2); // yields repeatedly for ~200ms
foreach (range(0, 5) as $value) {
echo $value . PHP_EOL;
}
});
$loop->defer(function () use ($loop) {
foreach (range(6, 9) as $value) {
echo $value . PHP_EOL;
}
});
$loop->run();6
7
8
9
0
1
2
3
4
5
The second task finishes first because the first one is sleeping. sleep() is a
busy-wait that yields on every iteration: siblings progress, but the loop
does not idle the CPU. sleep(0) (or any non-positive value) returns immediately
without yielding. See docs/caveats.md for the implications.
await() runs a task to completion and returns its value. From the main script
it drives the task synchronously:
$loop = new Loop();
$user = $loop->await(function () use ($loop) {
$loop->next(); // may yield while doing work
return ['id' => 42, 'name' => 'Ada'];
});
echo "user: {$user['id']} / {$user['name']}\n"; // user: 42 / AdaCalled from inside a task, await() yields to the scheduler while the awaited
sub-task works, so other tasks keep running in the meantime:
$loop = new Loop();
$loop->defer(function () use ($loop) {
echo "worker: awaiting a sub-task\n";
$sum = $loop->await(function () use ($loop) {
$total = 0;
foreach (range(1, 3) as $n) {
$total += $n;
$loop->next();
}
return $total;
});
echo "worker: sub-task returned $sum\n";
});
$loop->defer(function () use ($loop) {
foreach (['tick', 'tick', 'tick'] as $t) {
echo "heartbeat: $t\n";
$loop->next();
}
});
$loop->run();worker: awaiting a sub-task
heartbeat: tick
heartbeat: tick
heartbeat: tick
worker: sub-task returned 6
await() accepts a callable or a Fiber, started or not.
next() and sleep() must be called from within a fiber. Doing otherwise throws
InitPHP\FiberLoops\Exception\LoopException (a RuntimeException) with an
actionable message, instead of PHP's bare FiberError.
Full guides live in docs/:
| Guide | What it covers |
|---|---|
| Getting started | Install, your first two tasks, how the loop runs them. |
| Concepts | The scheduling model: fibers, the round-robin queue, cooperative yielding. |
| API reference | Every method, its signature, behaviour and edge cases. |
| await & concurrency | Awaiting sub-tasks from the main context and from inside a task. |
| Caveats & gotchas | Busy-wait sleep(), in-fiber preconditions, non-preemptive scheduling. |
composer install
composer test # PHPUnit
composer ci # cs-check + phpstan + testsContributions are welcome. Please read the org-wide Contributing guide and the Security policy. Fork, branch, add tests for your change, and open a pull request.
Copyright © 2022 InitPHP — released under the MIT License.
