Hi Edmond,
First of all, sorry for my bad English, and thanks a lot for the huge
amount of work you’ve put into this proposal.
You researched, wrote the RFC, implemented it, and answered tons of
questions. Really impressive.
I have one suggestion and two small questions.
Suggestion:
Maybe keep the base /Awaitable/ internal and expose two userland
interfaces that match the two cases described in the RFC:
|
// Single state change, idempotent read, same result on each await
interface Future extends Awaitable {}
// Multiple state changes, each await may observe a new state
interface Streamable extends Awaitable {}
This makes the single-shot vs multi-shot difference explicit and easier
for tools and libraries to reason about.
|
Questions (self-cancellation)
- What happens here?
|use function Async\spawn;
use function Async\suspend;
$coroutine = spawn(function() use (&$coroutine) {
$coroutine->cancel(new \Async\CancellationError("Self-cancelled"));
echo "Before suspend\n";
suspend();
echo "After suspend\n"; // should this run?
return "completed";
});
|
|await($coroutine);|
Can a cancelled coroutine suspend?
And if a function that yields is called after the cancel, should that
suspension still happen?
- And what about this one?
|use function Async\spawn;
$coroutine2 = spawn(function() use (&$coroutine2) {
$coroutine2->cancel(new \Async\CancellationError("Self-cancelled"));
echo "Before exception\n";
throw new \RuntimeException("boom after cancel");
});
|
|await($coroutine2);|
Which error does await() throw in this case — CancellationError or
RuntimeException?
It’d be great to clarify that in the docs, since it affects where people
put cleanup, logging, etc.
Again, thanks for the work, especially on such an important feature for
PHP’s future.
Hope to see it in php-src soon.
Best,
Luís Vinícius
Hi
1.5 RFC:
https://wiki.php.net/rfc/true_asyncHere’s the fifth version of the RFC with the updates made after the
1.4 discussion.Starting from 2025-11-03, there will be a two-week discussion period.
Changelog:
- Added FutureLike interface methods: cancel(), isCompleted(),
isCancelled()- Renamed Coroutine::isFinished() to Coroutine::isCompleted()
- Clarified exit/die behavior: always triggers Graceful Shutdown mode
regardless of where called- Added rationale for “Cancellable by design” policy: explains why
default cancellability reduces code complexity for read-heavy PHP
workloads- RFC structure improvements: reorganized Cancellation section with
proper subsections hierarchy- Moved “Coroutine lifetime” as subsection under Coroutine section
- Extended glossary with Awaitable, Suspension, Graceful Shutdown, and
Deadlock terms- Introduced FutureLike interface with single-assignment semantics and
changed await() signature to accept FutureLike instead of Awaitable
for type safety- Split RFC: Moved Scope and structured concurrency functionality to
separate Scope RFC. Base RFC now focuses on core async primitives
(coroutines, await, cancellation)I decided not to wait until Monday and made the changes today. If
anyone has read version 1.4 and has comments on it, they’re still
relevant.The Scope API has been moved to a separate RFC:
https://wiki.php.net/rfc/true_async_scope
Best Regards, Ed
Hello, Luís.
Maybe keep the base Awaitable internal and expose two userland interfaces that match the two cases described in the RFC:
Yes, that option is possible, but I want to share my current line of thinking:
- I’d really like to keep the name
Futurefor theFutureclass,
which is planned for upcoming RFCs. - I have some doubts that the name
Streamableprecisely conveys the
intended meaning (example Signals). However, if our colleagues who are
native English speakers say it’s a good choice, I have no objections.
Questions (self-cancellation) 1. What happens here?
Here’s what happens:
- The coroutine continues running successfully until completion.
- The code that calls
await()receives a
CancellationError("Self-cancelled")exception.
Originally, the idea was to throw an exception directly from
cancel(), but that would require adding an extra try-catch block.
So I decided to keep it silent instead.
Can a cancelled coroutine suspend?
Nothing will happen.
When cancel() is called, PHP detects that it’s the current coroutine
and only sets the exception as its result.
Later, when suspend() is invoked, it creates a new waker object that
has no exception, so after resuming, no exception occurs.
Do you think this case should be additionally described in the RFC?
So, that would give developers a better understanding.
Sometimes a coroutine doesn’t cancel itself but its own Scope,
and the developer should understand that if they cancel a Scope that
includes the current coroutine, they need to finish its work properly
from within.
Which error does await() throw in this case — CancellationError or RuntimeException?
RuntimeException, because this exception will override the previous one.
There is more logic here than described in the RFC, because there is
an “override” rule.
Exceptions of the CancellationError class do not override each
other. The first cancellation reason is always visible.
However, if an unhandled exception of another class occurs, it will
replace the CancellationError.
In other words, the logic follows these rules:
- The first cancellation reason takes precedence over subsequent ones.
- Unexpected errors take precedence over
CancellationError.
So, when a developer tries to cancel a coroutine more than once, it
has no effect.
It’d be great to clarify that in the docs, since it affects where people put cleanup, logging, etc.
This case should be described in more detail in the RFC.
Thank you for noticing it!
Again, thanks for the work, especially on such an important feature for PHP’s future.
Thank you for your excellent feedback, it’s very inspiring.
Best Regards, Ed
I also want to note another possible case: “cancellation after
cancellation,” which is not yet implemented.
The Scheduler API forbids canceling a coroutine twice.
However, the Scheduler itself has the right to do so and may use it in
a critical situation when it must urgently cancel all coroutines, even
those that were already canceled.
This is a rough approach, so the cancellation code might expect
certain conditions, but it is highly likely to be used in situations
such as DeadLock or similar cases.
Best Regards, Ed