Cancellation
In the previous example there was an interesting call to the cancel function,
use function Async\spawn;
use function Async\delay;
$progress = spawn(function() use (&$counter, $total) {
while (true) {
printProgress($counter, $total);
delay(1000);
}
});
processUsers('users.csv', $counter);
$progress->cancel();What happens if we remove it? Try it yourself. The $progress coroutine spins in an infinite loop with a 1-second delay. When processUsers finishes, control moves on. The $progress coroutine keeps running. Forever. It will never stop. The PHP process will never stop (unless it's killed from the outside).
$progress->cancel() stops the $progress coroutine. But how?
use function Async\spawn;
use function Async\delay;
$progress = spawn(function() use (&$counter, $total) {
while (true) {
printProgress($counter, $total);
try {
delay(1000);
} catch (Throwable $e) {
echo get_class($e). PHP_EOL;
throw $e;
}
}
});
processUsers('users.csv', $counter);
$progress->cancel();Let's change the code around delay(1000) and see what happens:
Async\AsyncCancellationWhen the $progress coroutine was asleep inside delay(1000) and cancel() was then called, delay threw an Async\AsyncCancellation exception. The same happens with a plain sleep(1): under TrueAsync sleep() becomes asynchronous too and is likewise a cancellation point — it throws Async\AsyncCancellation exactly like delay.
You could say that using delay in your code effectively establishes a contract that lets other code interrupt the coroutine's execution. This is very convenient, since it once again separates concerns between different modules:
- The coroutine doesn't know when its execution will be interrupted.
- The code that cancels the coroutine doesn't know exactly how the coroutine will be interrupted.
A coroutine doesn't stop by some kind of magic, it stops via an exception. A coroutine cannot be cancelled in the middle of some arbitrary operation, only at a point where it itself chooses to yield. This type of cancellation is called "cooperative".