A scheduler keeps a queue of runnable fibers and a list of timers, and resumes one fiber at a time. Loop::sleep() records a wake-up time for the current fiber and suspends it, handing control back to run():
<?php
final class Loop
{
private array $ready = [], $timers = []; // runnable fibers; [wake-up ns, fiber] pairs
public function spawn(callable $task): void { $this->ready[] = new Fiber($task); }
public function sleep(float $s): void {
$this->timers[] = [hrtime(true) + (int) ($s * 1e9), Fiber::getCurrent()];
Fiber::suspend(); // back to run(); a timer will wake us
}
public function run(): void {
while ($this->ready || $this->timers) {
foreach ($this->timers as $k => [$at, $f]) {
if ($at <= hrtime(true)) { $this->ready[] = $f; unset($this->timers[$k]); }
}
if (!$this->ready) { usleep(1000); continue; } // idle: wait for the next timer
$f = array_shift($this->ready);
$f->isStarted() ? $f->resume() : $f->start();
}
}
}
[$loop, $t0] = [new Loop(), hrtime(true)];
foreach (['A' => 0.3, 'B' => 0.1, 'C' => 0.2] as $name => $delay) {
$loop->spawn(function () use ($loop, $t0, $name, $delay) {
$loop->sleep($delay);
printf("%.2f s %s woke up\n", (hrtime(true) - $t0) / 1e9, $name);
});
}
$loop->run();Output
0.10 s B woke up 0.20 s C woke up 0.30 s A woke up
Waits of 0.3, 0.1 and 0.2 seconds overlapped, so all three were done at 0.3 s rather than 0.6 s. A real loop adds "resume me when this socket is readable" (next). In every library, one task that computes or blocks freezes the rest, and a fiber's exception surfaces in run().