Hi
1.5 RFC:
https://wiki.php.net/rfc/true_async
Here’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
Hi Edmond!
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
Thanks for the RFC update! I've been trying to read and understand this RFC
since its earlier versions and I can definitely feel it getting easier to
digest, which I can only assume it's a good thing for RFC voters - it's
easier to vote No because something is too complex / too hard to understand.
One minor question: is this section
https://wiki.php.net/rfc/true_async#awaiting_a_result_with_cancellation
named wrongly? I'm not sure how this snippet of code relates to
cancellation.
Onto more important things. In regards to the change of Awaitable vs
FutureLike, my understanding of the discussion is that on the
implementation side it was asked about whether an Awaitable object should
be awaited in a loop (consumed until completion) or if it should be awaited
only once, which also raised the question about idempotency and whether an
object is awaitable more than once. While the change makes the type-system
somewhat more explicit in regards to how await() is meant for a single-shot
Awaitable object (named FutureLike), it does mean the implementation of
things like awaitAll(Awaitable[] $awaitables) is no longer a simple loop to
await every item in the array.
My questions are:
- What's the difference between Multishot Awaitables and Generators?
- If await() is hardened to FutureLike only, doesn't this mean that
multishot awaitables are not really capable of being awaited anymore?
Doesn't this mean that the Awaitable interface becomes out-of-sync with the
await() function and it stops making sense? - Shouldn't FutureLike be Awaitable and what has been described as
Multishot awaitables should actually be generators / array of / list of
Awaitables?
In regards to Cancellable by Design. In the current state of PHP, we can
assume that if a function used to throw an Exception and a future version
stops throwing said exception it is not considered a BC Break. Of course,
throwing a different exception not part of the hierarchy is a BC break.
But when an exception signals "I cannot handle this" and a future version
becomes capable of handling it, that in essence is a feature/enhancement
and not treated as a BC break. With "Cancellation by Design" we are
expected that every coroutine be cancellable and that we must write try /
catch to design for cancellation. This seems to open up a different
development flow where catch blocks can be fundamentally part of the BC
promise. One possible alternative would be e.g. await(Awaitable $awaitable,
Closure $cancellation) where the cancellation of a coroutine would trigger
the specified Closure. Now, I don't want to dive too much into the
trade-offs of these options, what I want is to spark the idea that there
may be multiple ways to design cancellable coroutines. When I consider
that, plus the added fact that the RFC is very dense, extensive and hard to
digest, wouldn't it be best to postpone coroutine cancellations altogether?
The RFC itself states:
[...] read operations (database queries, API calls, file reads) are
typically as frequent as—or even more frequent than—write operations. Since
read operations generally don't modify state, they're inherently safe to
cancel without risking data corruption.
which not only I agree with, but also want us to focus on this very fact.
What if the first version of Async PHP provides userland with the ability
to trigger coroutines for reading purposes only? We will not forbid/prevent
anybody from shooting themselves if they want to, but we can still clearly
state the design principle that Async PHP is meant to spawn coroutines
cancellable by design. There will not be any coroutine markers in the
future nor there will be assumptions that coroutines written for the 1st
version of Async PHP must not be cancellable. The assumption is that the
first version of Async PHP should be treated as a way to perform read
operations only and a future RFC / enhancement will bring cancellation
capabilities (be it Closure, Try / Catch, what have you). My reasoning is
that this would further reduce the scope of the RFC while still introducing
real life useful async components to PHP, even if at a limited capacity. It
gives voters less concepts to understand, digest, agree on and approve and
it extends the opportunity to focus on specific deep aspects of Async PHP
in chunks and throughout different stages.
--
Marco Deleu
Hi
One minor question: is this section https://wiki.php.net/rfc/true_async#awaiting_a_result_with_cancellation named wrongly? I'm not sure how this snippet of code relates to cancellation.
Yes, second parameter is a CancellationToken. (spawn('sleep', 2)).
What's the difference between Multishot Awaitables and Generators?
There’s nothing in common between them.
It’s better to think of asynchronous objects as components of an
EventDriven pattern.
doesn't this mean that multishot awaitables are not really capable of being awaited anymore?
Exactly. It’s not needed for now.
Shouldn't FutureLike be Awaitable and what has been described as Multishot awaitables should actually be generators / array of / list of Awaitables?
FutureLike is a child interface of Awaitables.
we can assume that if a function used to throw an Exception and a future version stops throwing said exception it is not considered a BC Break.
This RFC does not change the behavior of existing functions. For
example, sleep works the same as before.
PHP functions that previously did not throw a CancellationError do not
throw it in this RFC either.
However, when you use functions (await example) that do throw these
exceptions, you handle them the same way as always. In that sense,
there’s no new paradigm.
What if the first version of Async PHP provides userland with the ability to trigger coroutines for reading purposes only?
Sorry, but I couldn’t understand why this needs to be done.
There’s nothing terrible about canceling write operations — no
“shooting yourself in the foot” either. A program can terminate at any
moment; that’s perfectly normal.
The concept of Cancellation by Design isn’t about guns — it’s about
reducing code. That’s all.
In the Swift language, for example, a different concept is used:
cancellation is always written explicitly in the code, like this:
func fetchData() async throws -> String {
for i in 1...5 {
try Task.checkCancellation()
print("Fetching chunk \(i)...")
try await Task.sleep(nanoseconds: 500_000_000)
}
return "Data loaded"
}
The only question is about how much code you write and the likelihood
of errors. The more code you write, the higher the chance of making
one.
Write safety is a different topic; it concerns how code is implemented
at the lowest level, meaning the code that calls OS functions.
Cancelling a coroutine cannot interrupt a kernel-level write operation
— it can only interrupt waiting for the write, and what to do next is
decided by the user-level code.
The cancellation design was implemented as early as 2015 in Python
(and in other languages as well) and has worked perfectly since then.
For languages like PHP, it’s a convenient and, most importantly,
familiar mechanism. The real problem is that an exception can
accidentally be caught without exiting the coroutine. For example:
catch (\Throwable $e) {
Logger::log();
}
continue;
Hi Edmond,
Am 30.10.25 um 9:19 AM schrieb Edmond Dantes:
Hi
1.5 RFC:
https://wiki.php.net/rfc/true_async
first of all thank you for investing so much time and effort into
improving PHP.
The True Async RFC changed a lot in the past iterations and removed a
lot of related but tangential topics. I really appreciate your
willingness to adapt to get the best possible outcome.
What I now see is as far as I understand essentially a Fiber 2.0 RFC so
I wonder if it would not be better to improve the available Fibers
instead of creating an incompatible second mechanism.
The Coroutine class is essentially a Fiber class that can be cancelled
and restricted by a cancellation awaitable.
The Fiber class could get startWithTimeout($timeout, ...$args) and
resumeWithTimeout($timeout, mixed $value = null) methods as well as a
cancel() method.
It wouldn't be a Fiber in the pure compsci way any more but I am willing
to accept that if it prevents us from having two ways for (semi)
cooperative multitasking.
The part about how the Awaitable and FutureLike interfaces work is very
unclear to me. They are there but they do not describe how they could be
used in a truly multitasking fashion. That is somehow open to the
Scheduler/Reactor which are not described to reduce the complexity.
The RFC as it is allows to await(new Coroutine()) which is syntactical
sugar for $fiber = new Fiber(); $fiber->start(); while (!$fiber->isTerminated()) $fiber->resume(); So a followup RFC would
need introduce this additional mechanism into these interfaces.
Also I do not really understand why the "cancellation" is an awaitable.
If the provided awaitable is itself some infinitely blocking Coroutine
(e.g. while (true) {}), how can the scheduler run the actual Coroutine
and the "cancellation" awaitable to determine whether the Coroutine
should be cancelled or not? As long as there is no multithreading, this
does not make sense for me.
In addition, what happens if a Coroutine is suspended and is restarted
again. Is the cancellation awaitable restarted? Or just continued?
I am really skeptical if the current RFC is the right way to go,
establishing a Coroutine and Awaitable and FutureLike interfaces in
competition to the existing Fiber.
I would rather see a step-by-step plan with gradual improvements like this:
-
Propose some changes to Fiber so it can be interrupted after a timer
expired and it can be cancelled. -
Add a unified polling mechanism for all kinds of IO events (timeouts
and signals included) like Jakub's "Polling API". -
Enhance the Fiber class so it can expose a PollHandle/Pollable that
it is currently waiting on, either as a property of the Fiber
(Fiber::$pollHandle) or as aFiber::suspendPolling(PollHandle $pollHandle, mixed $value = null)method. -
Now internal IO methods can be changed to start a pollable Fiber
instead of blocking the execution if they are started in a specific way
(e.g. by a then introduced spawn() call). -
With all that in place, userland can now create their own
Scheduler/Rector. The Core could also include a simple default
implementation used the PollContext/PollWatcher in addition with a
scheduling policy for other Fibers.
Kind regards
Dennis
Hello Dennis.
With all that in place, userland can now create their own Scheduler/Rector.
I once wrote about why such solutions are unsuccessful — and clearly
bad from PHP’s point of view.
But since I don’t remember where that text was, I’ll try to express it again.
Programming languages have levels of abstraction, which researchers
have been trying to quantify mathematically since the 1970s.
PHP is a high-level programming language with a relatively high degree
of abstraction, thanks to its memory management, built-in runtime, and
abstraction over the operating system.
Suppose someone created an RFC proposing to add assembly inserts into PHP.
Would you vote in favor of this RFC?
If not, why?
Most software — almost all of it — from operating systems to browsers,
is built on the principles of multilayered architecture,
where abstractions are separated into distinct layers.
This is done to reduce and control interdependencies, which directly
affects what are probably the three most important parameters in
programming:
the cost of code, the cost of debugging, and the cost of refactoring.
For this architecture to work, developers try to follow the Strict
Layering rule (known by other terms in different contexts),
which states that code from a higher-level layer must not interact
with a lower-level layer while skipping the intermediate one.
Although this rule is almost always violated, adhering to it is
justified in most cases.
Assembly inserts in PHP violate the Strict Layering rule and give
the programmer the ability to completely break the language’s
operation, since they belong to the lowest layer.
A Fiber in PHP is a context that stores a pointer to the C stack, CPU
registers, and part of the VM state, combined with a generator.
Fibers cannot be used as coroutines because this approach is
inefficient in terms of performance and memory.
The reason is that a Fiber is an extremely low-level primitive — only
slightly higher than assembly.
Let’s recall what a full-fledged abstraction of asynchrony looks like
in any proper programming language:
https://editor.plantuml.com/uml/TLBDRjGm4BxFKunw0QJTvSu1LQfQgH9L4HLmTftPpQYE7SrCkXigtXqx8MxOYfF7zZVVZyUNQaviw0Be4yVUYUimS2GRUy97-iKa0COM2B-HyvPasmW_KyIh96cm3CMxr5004FBcuY4ZBwvFvFDTAgXeT39yVyEF91ykq6azUm74LLCbd41r1x__eNxmBJL389bGTOSlPxZPxOpwMvyBNkSOXbzIwWjgAWh9Exm9wOXfZxJ4WDUms-tdolS9tT6nuUt7UwH21ijDHbLl1HUJyNv48TUCw6kqIlkcOMGA3QQ8aKusom1a5aBXGsl5NOL3hJ9rD4b1NpKmy9xyw0FjuDPG1-qfDYk05fKvXuiD2kdGaOArrE6nfLZJ2lL9JASGfKztGBcXc3gpjamOtlw3DeKi_kCErPpH1lBYdpQJyjNNxoXqO3KItQtUPXu3AHxPMex8zb_bnMmTH9SYvrLnpu6m8VN2VJdOW76NXMPjvKDqGV6P7Tu_xE1d2UxYF5LCtWy5oJOFacdryrPUBdCrTE4F
Therefore, Fibers violate the Strict Layering principle, and PHP
has no way to prevent this — unlike Rust, for example, where you can
hide parts of the implementation within a crate (package).
Even in C++, there is no such violation — the programmer works with
the coroutine abstraction.
It is also important to understand the difference between PHP and
C++/Rust: PHP is a single-runtime language.
When you have multiple runtime libraries, for example asynchronous
ones, a segmentation problem arises:
you cannot simply use code written for runtime A in runtime B, because
the runtimes are incompatible!
What made PHP popular?
What made Go popular?
That’s right — the built-in runtime!
Just write code.
Just a week or two ago, I read an article from the Python community
discussing the problems caused by the segmentation of asynchronous
libraries.
And let me remind you, that Python has had built-in language-level
support for asynchrony since 2015.
So...
I’m convinced that PHP should remain a high-level language with a
built-in runtime (at least as long as it’s interpreted).
Attempts to create a “backdoor” in the language to let libraries
implement what should be written in C bring no benefit to PHP users.
After all, to use asynchrony, it’s not enough to just add spawn or coroutines.
Libraries and frameworks must also be adapted, and all of that takes time.
Go added more abstractions to the language to make writing business
logic easier.
Python will soon implement JIT.
People will choose the tool that “just works” with minimal effort and
they won’t care what it’s called.
Best regards,
Ed
Hi Edmond,
thank you for your reply.
Am 01.11.25 um 8:32 AM schrieb Edmond Dantes:
A Fiber in PHP is a context that stores a pointer to the C stack, CPU
registers, and part of the VM state, combined with a generator.
Fibers cannot be used as coroutines because this approach is
inefficient in terms of performance and memory.
The reason is that a Fiber is an extremely low-level primitive — only
slightly higher than assembly.Therefore, Fibers violate the Strict Layering principle ...
From the standpoint of PHP language user, I have a completely different
view on Fibers vs. Corotines.
They look very similar from the outside and if we talk about
abstractions, that is the point that matters as the inner workings are
hidden.
I really belief we should avoid fragmentation and enhance/adjust Fibers
to meet the memory and performance requirements of a Coroutine.
Thanks
Dennis
From the standpoint of PHP language user, I have a completely different view on Fibers vs. Corotines.
That’s sad.
They look very similar from the outside
Coroutines and Fibers have completely different behavior. I hope
you’re not comparing them just by appearance?
I really belief we should avoid fragmentation and enhance/adjust Fibers to meet the memory and performance requirements of a Coroutine.
But the problem has already happened, and it’s not directly related to this RFC.
Of course, there’s a possibility to bridge the two worlds by calling
PHP functions from C, but as I’ve said before: just because something
can be done doesn’t mean it should be done.
If the provided awaitable is itself some infinitely blocking Coroutine (e.g. while (true) {}),
If you have a coroutine with an infinite loop, it means other
coroutines will never get control.
(more about it by searching for the keyword: “concurrency")
The RFC contains an example that isn’t very elegant from a semantic
point of view, but is completely correct in terms of logic:
// Await task 1, but no longer than 5 seconds.
await($task1, spawn(sleep(...), 5));
And here’s another piece of code (Async\Signal is not present in the
RFC, but it’s entirely possible.):
// Await task 1 until a signal occurs.
await($task1, new Async\Signal(SIG_TERM));
In addition, what happens if a Coroutine is suspended and is restarted again.
The Await function waits for the coroutine to complete.
The suspended state does not affect the waiting process.
The wait is interrupted for two reasons: an unhandled exception or the
coroutine’s completion.
All of this is described in the RFC: https://wiki.php.net/rfc/true_async#await
Ed
So...
I’m convinced that PHP should remain a high-level language with a
built-in runtime (at least as long as it’s interpreted).
Attempts to create a “backdoor” in the language to let libraries
implement what should be written in C bring no benefit to PHP users.
In concept, I agree. Which is part of why I want to see an Async RFC that goes even higher than the current one, not lower. :-)
But that is separate from the question of whether it's possible to build on Fibers, rather than effectively deprecate them in practice if not in name.
--Larry Garfield
Hi
But that is separate from the question of whether it's possible to build on Fibers, rather than effectively deprecate them in practice if not in name.
Originally, Fiber was proposed with a Scheduler, but the Scheduler was
refused.
To allow Fiber switching without a Scheduler, they were made
symmetric (so that the switching code could do it manually).
This, in turn, creates a problem when trying to add a Scheduler later.
To create coroutines, you need to write the "switching code".
But to write the switching code, Fibers must be allowed to switch arbitrarily.
And for Fibers to switch arbitrarily, backward compatibility must be broken.
What could be reused? The context-switching code and the observer
component handlers — these were reused.
The issue isn’t that Fiber behavior can’t be changed, but that it
should be hidden as an internal component.
There should be no access to it from PHP code.
The experience with Fiber shows that language features like asynchrony
must be designed as a whole from the start — thoughtfully and
consistently.
You can’t make a language "a little" asynchronous today and a bit
"more" tomorrow:
- Critical components must be designed in advance to understand
how they interact. - They must be placed within the same layer of abstraction.
- Use cases must be thought out in advance.
Best regards
Ed
Hi Edmond,
could you please clarify these two questions? Thanks.
Am 31.10.25 um 11:59 PM schrieb Dennis Birkholz:
Also I do not really understand why the "cancellation" is an
awaitable. If the provided awaitable is itself some infinitely
blocking Coroutine (e.g.while (true) {}), how can the scheduler run
the actual Coroutine and the "cancellation" awaitable to determine
whether the Coroutine should be cancelled or not? As long as there is
no multithreading, this does not make sense for me.In addition, what happens if a Coroutine is suspended and is restarted
again. Is the cancellation awaitable restarted? Or just continued?
Kind regards
Dennis