PHP True Async RFC Stage 5

php.internals

Edmond Dantes

306 days ago
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

Deleu

306 days ago
Hi Edmond! On Thu, Oct 30, 2025 at 5:22 AM Edmond Dantes <edmond.ht@gmail.com> wrote:
> 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 >
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

Edmond Dantes

306 days ago
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: ```swift 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: ```php catch (\Throwable $e) { Logger::log(); } continue; ```

Dennis Birkholz

304 days ago
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: 1. Propose some changes to Fiber so it can be interrupted after a timer expired and it can be cancelled. 2. Add a unified polling mechanism for all kinds of IO events (timeouts and signals included) like Jakub's "Polling API". 3. 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 a `Fiber::suspendPolling(PollHandle $pollHandle, mixed $value = null)` method. 4. 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). 5. 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

Edmond Dantes

304 days ago
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

Dennis Birkholz

304 days ago
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

Edmond Dantes

304 days ago
> 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: ```php // 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.): ```php // 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

Larry Garfield

304 days ago
On Sat, Nov 1, 2025, at 2:32 AM, Edmond Dantes wrote:
> 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

Edmond Dantes

304 days ago
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: 1. Critical components must be designed **in advance** to understand how they interact. 2. They must be placed within the **same layer of abstraction**. 3. Use cases must be thought out in advance. --- Best regards Ed

Dennis Birkholz

304 days ago
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

Luís Vinícius Santos da Costa Barros

298 days ago
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. Later on, it could even be extended with something like: |interface Retryable extends Awaitable {}| *Questions (self-cancellation)* 1. 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? 2. 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*

Edmond Dantes

292 days ago
Hello all. Today marks two weeks since the RFC was published. I need to apply a few minor fixes that Luis pointed out. If anyone else is working on comments for the RFC, please let me know. If there are no objections, we can start the vote on Monday. Best regards, Ed

Tim Düsterhus

292 days ago
Hi Given your planned timeline of voting, I wanted to chime in here before my vacation. I'll likely only see the reply on Monday morning. On 11/13/25 10:01, Edmond Dantes wrote:
> If anyone else is working on comments for the RFC, please let me know. > If there are no objections, we can start the vote on Monday.
It appears that you believe that the RFC and the proposal finally settled. I frankly lost track of what has been discussed in all the different discussion threads related to various Async RFCs. What I am missing from the RFC text is some kind of "Executive Summary" to make it clear what *is* and what *is not* actually being proposed. The RFC starts of with goals and a glossary that primarily explains by means of an example. This makes it hard for me to see what I am actually voting for (and what I am not), especially after the many changes to refine the RFC. I would suggest to add a “full stub” (as suggested in the RFC template https://wiki.php.net/rfc/template#proposal) at the start and also to shortly explain what is proposed and what is left untouched (e.g. the RFC already mentions that file_get_contents is not proposed to change, but that is easy to miss without carefully reading everything) before diving into the details for each of the functions. With regard to the relationship with fibers, it is not clear to me why e.g. Fiber::suspend() could map to Async\suspend() and why the Async event loop couldn't call ->resume() on suspended Fibers. Elaborating a little more would be helpful I think. Also, please make sure to add the “Abstain” option to the vote (https://wiki.php.net/rfc/rfc_vote_abstain). Best regards Tim Düsterhus

Edmond Dantes

292 days ago
Hello, Tim.
> What I am missing from the RFC text is some kind of "Executive Summary" > to make it clear what *is* and what *is not* actually being proposed
I’ll try to look through the text again to see what exactly might be unclear. Although to be honest, I also don’t really understand what exactly is unclear.
> This makes it hard for me to see what I am actually voting for
I suggest we do it this way. I’ll write a summary here, and if needed we can add some of these phrases to the RFC. I have an idea to format the summary as questions and answers. That will probably make it easier to understand.
> I would suggest to add a “full stub”
+
> Also, please make sure to add the “Abstain” option to the vote
I will do it, thanks
> With regard to the relationship with fibers
I will also take these questions into account in the next message. Thanks, Ed

Edmond Dantes

292 days ago
Hello. Mini Faq: https://github.com/true-async/php-true-async-rfc/blob/main/faq.md Text here: ## Executive Summary: What This RFC Proposes This RFC proposes adding **built-in concurrency support** to PHP through two core components: ### 1. Coroutines via `spawn()` Launch any PHP function as a lightweight coroutine that can be suspended and resumed: ```php use function Async\spawn; use function Async\await; $coroutine = spawn(file_get_contents(...), 'https://php.net'); $result = await($coroutine); ``` ### 2. Non-blocking I/O Functions **50+ existing PHP functions** automatically become non-blocking when used inside coroutines: - **Database**: PDO MySQL, MySQLi operations - **Network**: CURL, sockets, streams, DNS lookups - **Files**: `file_get_contents()`, `fread()`, `fwrite()` - **Process**: `exec()`, `shell_exec()`, `proc_open()` - **Timers**: `sleep()`, `usleep()` **Key principle:** From the developer's perspective, these functions work identically to their synchronous versions. The difference is that they suspend only the current coroutine instead of blocking the entire PHP process. See full list: https://github.com/true-async/php-async#adapted-php-functions ### What This RFC Does NOT Propose - **No changes to existing synchronous behavior** - code without coroutines works exactly as before - **No new syntax keywords** - uses function calls (`spawn()`, `await()`, `suspend()`) - **No changes to Fiber API** - Fibers and True Async are mutually exclusive by design - **No structured concurrency primitives** - covered in separate [Scope RFC](https://wiki.php.net/rfc/true_async_scope) ## General Questions ### Q: What is the main goal of this RFC? **A:** The RFC aims to provide a standardized way to write concurrent code in PHP without requiring developers to rewrite existing synchronous code. The key value proposition is that existing code works **exactly the same** inside a coroutine without modifications, unlike explicit async/await models. ### Q: How is this different from Fibers? **A:** Fibers and True Async serve fundamentally different purposes and cannot coexist: **Fibers:** - Low-level symmetric execution contexts - Programmer explicitly controls switching (`$fiber->resume()`, `Fiber::suspend()`) - Direct access to execution stack management - Suitable for building custom scheduling solutions **True Async:** - High-level asymmetric coroutines - Automatic switching managed by the scheduler - Transparent to the developer - Designed for business logic, not infrastructure **Why they can't work together:** 1. **Resource conflicts**: Both manage the same low-level resources (execution context, CPU stack) in incompatible ways 2. **Architectural incompatibility**: Mixing symmetric (Fibers) and asymmetric (coroutines) models creates unpredictable behavior 3. **Abstraction level**: Fibers expose low-level primitives in a high-level language, violating the "Strict Layering" principle **Why not map Fiber::suspend() to Async\suspend()?** This would create a leaky abstraction: - Fibers require explicit scheduling decisions (who to resume? when?) - True Async scheduler makes these decisions automatically - Mixing both models would break scheduler guarantees and lead to race conditions - The execution models are fundamentally incompatible (symmetric vs asymmetric) If you need Fibers' explicit control, use Fibers. If you want automatic concurrency for I/O-bound applications, use True Async. Attempting to unify them would result in a solution that's neither simple nor safe. ### Q: Isn't this just Fibers 2.0? **A:** No. While both deal with execution contexts: - Fibers require explicit switching and manual control - True Async provides automatic scheduling and high-level primitives - They solve different problems at different abstraction levels - They are mutually exclusive by design ### Q: Can I use this with FPM? **A:** Yes! True Async works in all execution modes including FPM. The reactor activates within the context of `php_request_startup/php_request_shutdown()`, requiring no SAPI modifications. ### Q: What about `exit` and `die`? **A:** They always trigger **Graceful Shutdown** mode: - All coroutines in globalScope are cancelled - Application continues execution without restrictions to shut down naturally - This allows proper cleanup operations ### Q: Do I need to rewrite my existing code? **A:** No. The main value of this implementation is that existing synchronous code works inside coroutines without modification. You can gradually adopt async features where beneficial. ### Q: How does this RFC affect I/O functions? **A:** From the coroutine's perspective, I/O functions **do not change their behavior** they work exactly as they always have. However, functions that previously blocked the entire PHP process now only suspend the current coroutine, allowing other coroutines to continue executing. ---- Best regards, Ed

Jakub Zelenka

292 days ago
Hi, On Thu, Nov 13, 2025 at 10:02 AM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hello all. > > Today marks two weeks since the RFC was published. > > I need to apply a few minor fixes that Luis pointed out. > > If anyone else is working on comments for the RFC, please let me know. > If there are no objections, we can start the vote on Monday. > >
I thought about it and I think we should have this with implementation otherwise the whole thing is kind of pointless because we might not get it merged if some internals disagreement happens. It will be also impossible to get anything bigger than what is proposed now reviewed (read that we won't be likely able to merge the complete implementation in one go). In other words if this passes, it will just means that the API is ok but there is nothing actionable. In addition, I think some people might be even voting against it if there is no implementation that they can review. It means there should be a PR implementing exactly what is in this RFC (minimal stripped version of your current implementation) IMO. Cheers Jakub

Edmond Dantes

292 days ago
Hello, Jakub.
> It means there should be a PR implementing exactly what is in this RFC (minimal stripped version of your current implementation) IMO.
This RFC includes not only the API, but also the Scheduler, Reactor, and non-blocking versions of PHP functions. Removing Scope from the public classes doesn’t really change that. Are you saying that some other implementation is needed as well? Best regards, Ed

Jakub Zelenka

292 days ago
Hi, On Thu, Nov 13, 2025 at 11:44 AM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hello, Jakub. > > > It means there should be a PR implementing exactly what is in this RFC > (minimal stripped version of your current implementation) IMO. > > This RFC includes not only the API, but also the Scheduler, Reactor, > and non-blocking versions of PHP functions. Removing Scope from the > public classes doesn’t really change that. > Are you saying that some other implementation is needed as well? > >
I think it would be good to see the implementation that can cover the currently proposed API and try to strip it as much as possible so it doesn't contain much more than that. We saw that PR for the async API was already quite big and we didn't really get any agreement there partially also because there was no user of that and it was not possible to have any tests for it (without writing them in C). So what I'm thinking is that if some minimal version that implements just this (e.g. reactor can be just dummy because there is no io atm. and other things can be stripped too), then the voters would get better idea what they are dealing with and could even try it out. Cheers Jakub

Edmond Dantes

292 days ago
Hello Jakub.
> I think it would be good to see the implementation that can cover the currently proposed API and try to strip it as much as possible so it doesn't contain much more than that. We saw that PR for the async API was already quite > big and we didn't really get any agreement there partially also because there was no user of that and it was not possible to have any tests for it (without writing them in C). > So what I'm thinking is that if some minimal version that implements just this (e.g. reactor can be just dummy because there is no io atm. and other things can be stripped too), then the voters would get better idea what they are > dealing with and could even try it out.
I understand what you mean. ** Regarding simplifying the code.** Any “simplification” essentially comes down to removing stub files and the C classes that implement the PHP classes. This is a relatively small part of the project. For example, removing Scope from the C code doesn’t make much sense, because it turned out (even unintentionally) to be a very convenient structure for tracking a group of coroutines. In other words, no major changes to the code are expected before the PR review begins. ** Reactor. ** Since the reactor uses libUV and we currently do not plan to provide a pure-C implementation, we agreed to move it into a separate library. ** Testing. ** Some functions that do not involve I/O can be covered by unit tests. This is a small portion of the API. However, covering all the remaining code that performs I/O with unit tests is not practical. We could try to emulate the OS and so on… but I think you understand that the result is not worth the effort. This means we cannot avoid integration tests. P.S. Personally, I would prefer to agree on the PR first and the RFC afterwards. As I mentioned earlier, the code is more important than the RFC, because it defines the real relationships and logic, while the RFC only “describes” them. But we live in a world that follows its own rules. --- Ed

Jakub Zelenka

289 days ago
Hi, On Thu, Nov 13, 2025 at 4:25 PM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hello Jakub. > > > I think it would be good to see the implementation that can cover the > currently proposed API and try to strip it as much as possible so it > doesn't contain much more than that. We saw that PR for the async API was > already quite > > big and we didn't really get any agreement there partially also because > there was no user of that and it was not possible to have any tests for it > (without writing them in C). > > So what I'm thinking is that if some minimal version that implements > just this (e.g. reactor can be just dummy because there is no io atm. and > other things can be stripped too), then the voters would get better idea > what they are > dealing with and could even try it out. > > I understand what you mean. > > ** Regarding simplifying the code.** > Any “simplification” essentially comes down to removing stub files and > the C classes that implement the PHP classes. This is a relatively > small part of the project. >
It's 1.6k lines so it might help a little bit
> For example, removing Scope from the C code doesn’t make much sense, > because it turned out (even unintentionally) to be a very convenient > structure for tracking a group of coroutines. > In other words, no major changes to the code are expected before the > PR review begins. >
I don't think you can create PR with the whole project. It's not gonna get reviewed and merged. It might not even open in GH. So you will need to come up with a way how to split to small pieces and I think this is the first self contained bit that should be offered in minimal form.
> > ** Reactor. ** > Since the reactor uses libUV and we currently do not plan to provide a > pure-C implementation, we agreed to move it into a separate library. > >
Why do you need reactor for this specific part of proposal? The thing is that there shouldn't be any IO so you reduce scheduler code as well and make it simpler and more reviewable. Kind regards, Jakub

Edmond Dantes

289 days ago
Hello
> It's 1.6k lines so it might help a little bit
Yeah :)
> Why do you need reactor for this specific part of proposal? The thing is that there shouldn't be any IO so you reduce scheduler code as well and make it simpler and more reviewable.
So you are suggesting removing all I/O from the RFC. On the one hand, that sounds appealing. It immediately eliminates a bunch of tests. But there is a trap we could all fall into. By separating the reactor and the scheduler, as well as the rules of how they work together, we might accidentally introduce an error into the document simply because the documents would be split. (interface drift) The fact that the I/O rules and coroutine rules are part of a single document developed together is actually an advantage, just as the existence of separate RFCs for await and Scope is. And this does not prevent splitting the implementation code into many small parts. On the other hand, STREAM has corner cases, especially in error situations, that would be appropriate to discuss and formalize in a separate RFC. Ideally, this should probably be done right before accepting the STREAM PR. However, such a description does not carry significant risks. This requires some thought.

Jakub Zelenka

289 days ago
On Sat, Nov 15, 2025 at 11:03 PM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hello > > > It's 1.6k lines so it might help a little bit > Yeah :) > > > Why do you need reactor for this specific part of proposal? The thing is > that there shouldn't be any IO so you reduce scheduler code as well and > make it simpler and more reviewable. > > So you are suggesting removing all I/O from the RFC. On the one hand, > that sounds appealing. It immediately eliminates a bunch of tests. But > there is a trap we could all fall into. > By separating the reactor and the scheduler, as well as the rules of > how they work together, we might accidentally introduce an error into > the document simply because the documents would be split. > (interface drift) >
I don't see it that way. You have already implementation showing that this is workable for the proposed user interface so it's just addition into that. I'm not sure how it could even impact user interface that is being proposed? If you are talking about internal interface, then it doesn't matter, because this can be changed (you don't have to keep BC there especially for such a new internal interface like this).
> The fact that the I/O rules and coroutine rules are part of a single > document developed together is actually an advantage, just as the > existence of separate RFCs for await and Scope is. >
But are I/O rules really part of this document? There are just a few mentioning of I/O and that seems more like a leftover from previous version to me. I don't think it needs to keep reactor in there and mention I/O in other parts. It should just clearly put it to the future scope to make clear that this is something that is part of the plan. Kind regards, Jakub

Edmond Dantes

291 days ago
Hello all. I’ve updated the RFC by adding a brief summary at the very beginning and adjusting the voting section. As far as I understand, opening the vote requires creating a separate page on the Wiki. For some reason, I couldn’t find clear instructions for this in the documentation, which is a bit surprising. Regarding the timeline, there is no need to rush. Someone may still want to share their thoughts and simply hasn’t had the chance yet. Therefore, we can extend the period until next Thursday. In the meantime, I may also find a few more issues in the document. --- Best regards, Ed

Rowan Tommins [IMSoP]

291 days ago
On 14 November 2025 09:49:52 GMT, Edmond Dantes <edmond.ht@gmail.com> wrote:
>As far as I understand, opening the vote requires creating a separate >page on the Wiki. For some reason, I couldn’t find clear instructions >for this in the documentation, which is a bit surprising.
The voting stage is described in step 6 of the How-To here: https://wiki.php.net/rfc/howto There's no separate page, you just edit the attributes on the voting widget tag. You can see how it looks on the two RFCs that are currently "in voting" on the RFC index: https://wiki.php.net/rfc Rowan Tommins [IMSoP]

Rob Landers

290 days ago
On Thu, Nov 13, 2025, at 10:01, Edmond Dantes wrote:
> Hello all. > > Today marks two weeks since the RFC was published. > > I need to apply a few minor fixes that Luis pointed out. > > If anyone else is working on comments for the RFC, please let me know. > If there are no objections, we can start the vote on Monday. > > Best regards, Ed
I have concerns about the clarity of when suspension occurs in this RFC. The RFC states as a core goal: "Code that was originally written and intended to run outside of a Coroutine must work EXACTLY THE SAME inside a Coroutine without modifications." And: "A PHP developer should not have to think about how Coroutine switch and should not need to manage their switching—except in special cases where they consciously choose to intervene in this logic." However, the RFC doesn’t clearly define what these "special cases" are or provide guidance on when developers need to intervene. Specific questions: 1. CPU-bound operations: If I have a tight loop processing data in memory (no I/O), will it monopolise the coroutine scheduler? Do I need to manually insert `suspend()` calls? How do I know when and where? 2. The RFC suggests that existing PHP functions won’t automatically be non-blocking. So which will? Is there a way to identify suspension points at the language/API level? 3. Performance implications: Without knowing where suspensions occur, how do developers avoid either: - Starving other coroutines (too few suspensions) - Degrading performance (too many suspensions) With explicit async/await ("coloured functions"), developers know exactly where suspension can occur. This RFC’s implicit model seems convenient, but without clear rules about suspension points, I’m unclear how developers can write correct concurrent code or reason about performance. Could the RFC clarify the rules for when automatic suspension occurs versus when manual `suspend()` calls are required? Is this RFC following Go’s model where suspension timing is an implementation detail developers shouldn’t rely on? If so, that should be stated explicitly. Keep in mind that Go didn’t start that way and took nearly a decade to get there. Earlier versions of Go explicitly stated where suspensions were. — Rob

Edmond Dantes

290 days ago
Hello all. Some of these questions sound familiar. Let’s try to sort them out.
> If I have a tight loop processing data in memory (no I/O), will it monopolise the coroutine scheduler? Do I need to manually insert suspend() calls? How do I know when and where?
A coroutine must yield control on its own. If it keeps it through an endless loop, then it will be the only one running.
> The RFC suggests that existing PHP functions won’t automatically be non-blocking. So which will? Is there a way to identify suspension points at the language/API level?
The RFC does not say that. PHP I/O functions automatically become non-blocking relative to the whole process. In other words, an I/O function calls suspend() on its own when needed. The programmer writes code exactly as before, under the illusion that operations are executed one after another.
> Performance implications: Without knowing where suspensions occur, how do developers avoid either:
In most cases, this is not the developer’s concern. Situations where performance is critical should be handled with dedicated tools. A PHP developer should not have to drop down to the C level. Properly designed abstractions must provide the required performance. I can already anticipate the question: but a developer could write something like “for i < 10000 suspend” or something similar. The answer is this: a developer must know how to use abstractions. As always. Everywhere. In any area of programming. It’s just as important as respecting proper layering in code. Provided that PHP does not try to play the role of a C-level language (there have already been such attempts, and they keep resurfacing), and does not try to play a web server or a database system. For most web scenarios, the current approach is more than sufficient. This has been proven by Swoole, which has been on the market for many years. Therefore, performance questions are outside the scope of this RFC. As for the concurrency model, let me remind you that Go has true multitasking. Goroutines in Go, although they have a “synthetic” stack. But you already know all this well. This RFC and its implementation describe coroutines in a single thread. That is very far from what Go provides. There is no preemptive multitasking in this RFC because it is completely unrelated. This RFC and its implementation provide cooperative concurrency in a single thread, where coroutine code yields control on its own. Why was this model chosen? The simple answer is: because it is the only one that can realistically be implemented within a finite timeframe. Any more questions? -- Ed

Rob Landers

290 days ago
On Sat, Nov 15, 2025, at 13:16, Edmond Dantes wrote:
> Hello all. > > Some of these questions sound familiar. Let’s try to sort them out. > > > If I have a tight loop processing data in memory (no I/O), will it monopolise the coroutine scheduler? Do I need to manually insert suspend() calls? How do I know when and where? > > A coroutine must yield control on its own. If it keeps it through an > endless loop, then it will be the only one running. > > > The RFC suggests that existing PHP functions won’t automatically be non-blocking. So which will? Is there a way to identify suspension points at the language/API level? > > The RFC does not say that. PHP I/O functions automatically become > non-blocking relative to the whole process. In other words, an I/O > function calls suspend() on its own when needed. The programmer writes > code exactly as before, under the illusion that operations are > executed one after another. > > > Performance implications: Without knowing where suspensions occur, how do developers avoid either: > > In most cases, this is not the developer’s concern. Situations where > performance is critical should be handled with dedicated tools. > A PHP developer should not have to drop down to the C level. Properly > designed abstractions must provide the required performance. > > I can already anticipate the question: but a developer could write > something like “for i < 10000 suspend” or something similar. > The answer is this: a developer must know how to use abstractions. As > always. Everywhere. In any area of programming. > It’s just as important as respecting proper layering in code. > > Provided that PHP does not try to play the role of a C-level language > (there have already been such attempts, and they keep resurfacing), > and does not try to play a web server or a database system. > For most web scenarios, the current approach is more than sufficient. > This has been proven by Swoole, which has been on the market for many > years. Therefore, performance questions are outside the scope of this > RFC. > > As for the concurrency model, let me remind you that Go has true > multitasking. Goroutines in Go, although they have a “synthetic” > stack. But you already know all this well. > This RFC and its implementation describe coroutines in a single > thread. That is very far from what Go provides. > > There is no preemptive multitasking in this RFC because it is > completely unrelated. > This RFC and its implementation provide cooperative concurrency in a > single thread, where coroutine code yields control on its own. > Why was this model chosen? The simple answer is: because it is the > only one that can realistically be implemented within a finite > timeframe. > > Any more questions? > > -- Ed
Hey Ed, I feel like we’re talking past each other a bit. I’m not questioning the choice of a cooperative scheduler, that’s totally fine, I’m trying to understand what the actual suspension points are. Every other language/runtime with cooperative concurrency spells this out, because without it developers can’t reason about performance or why a deadlock is happening. Based on the conversation so far, I’d imagine the list to look something like: - network i/o (streams/sockets/dns/curl/etc) - sleeps - subprocess waits? `proc_open` and friends? - extensions with a reactor implementation - awaiting `FutureLike` - `suspend()` If that’s the intended model, it’d help to have that spelled out directly; it makes it immediately clear which functions can or will suspend and prevents surprises. I also think the RFC needs at least minimal wording about scheduler guarantees, even if the details are implementation-specific. For example, is the scheduler run-to-suspend? FIFO or round-robin wakeup? And non-preemptive behaviour only appears here in the thread. It isn’t mentioned in the RFC itself. That’s important for people writing long, CPU-bound loops, since nothing will interrupt them unless they explicitly yield. Lastly, cancellation during a syscall is still unclear. If a coroutine is cancelled while something like `fwrite()` or a DB write is in progress, what should happen? Does `fwrite()` still return the number of bytes written? Does it throw? For write-operations in particular, this affects whether applications can maintain a consistent state. Clarifying these points would really help people understand how to reason about concurrency with this API. — Rob

Edmond Dantes

290 days ago
Hello.
> Based on the conversation so far, I’d imagine the list to look something like:
Yes, that’s absolutely correct. When a programmer uses an operation that would normally block the entire thread, control is handed over to the Scheduler instead. The suspend function is called inside all of these operations.
> If that’s the intended model, it’d help to have that spelled out directly; it makes it immediately clear which functions can or will suspend and prevents surprises.
In the Async implementation, it will be specified which functions are supported.
> I also think the RFC needs at least minimal wording about scheduler guarantees, even if the details are implementation-specific.
The Scheduler guarantees that a coroutine will be invoked if it is in the queue.
> For example, is the scheduler run-to-suspend? FIFO or round-robin wakeup? And non-preemptive behaviour only appears here in the thread. It isn’t mentioned in the RFC itself.
In Go, for example, when it was still cooperative, these details were also not part of any public contract. The only guarantee Go provided was that a coroutine would not be interrupted arbitrarily. The same applies to this RFC: coroutines are interrupted only at designated suspension points. However, neither Go nor any other language exposes the internal details of the Scheduler as part of a public contract, because those details may change without notice.
> That’s important for people writing long, CPU-bound loops, since nothing will interrupt them unless they explicitly yield.
Hypothetically, in the future it may become possible to interrupt loops, just like Go eventually did. This would likely require an additional RFC. PHP does have the ability to interrupt a loop at any point, but most likely only for terminating execution. This RFC does nothing of the sort.
> Lastly, cancellation during a syscall is still unclear. If a coroutine is cancelled while something like fwrite() or a DB write is in progress, what should happen? > Does fwrite() still return the number of bytes written? Does it throw? For write-operations in particular, this affects whether applications can maintain a consistent state.
If the write operation is interrupted, the function will return an error according to its contract. In this case, it will return false.
> Clarifying these points would really help people understand how to reason about concurrency with this API.
This is described in the document. There is, of course, a nuance regarding extended error descriptions, but at the moment no such changes are planned.

Edmond Dantes

290 days ago
As for:
> For example, is the scheduler run-to-suspend? > And non-preemptive behaviour only appears here in the thread. It isn’t mentioned in the RFC itself.
There is no direct statement in the RFC that cooperative multitasking is implemented. I think this text was removed, and that needs to be fixed. But on the other hand, there is a clear description of the contract expressed in different words: RFC: "A coroutine can stop itself passing control to the scheduler. However, it cannot be stopped externally." which essentially means the same thing. This is exactly what constitutes the public contract between PHP and the developer. I included a list of I/O functions for demonstration purposes, but this list is not part of the RFC. It is part of the implementation. This means that not all I/O functions can or should be adapted immediately.

Weedpacket

289 days ago
On 2025-11-16 04:21, Edmond Dantes wrote:
> > RFC: "A coroutine can stop itself passing control to the scheduler. > However, it cannot be stopped externally." > > which essentially means the same thing. >
Oh ... there's an ambiguity in that line. "I'm now going to pass control to the scheduler ... no, no I won't; that's exactly what they'll be expecting me to do." I suggest rewriting the sentence as: "A coroutine can stop itself and pass control to the scheduler." or "A coroutine can stop itself, passing control to the scheduler."

Edmond Dantes

289 days ago
Hello, Morgan
> I suggest rewriting the sentence as: > "A coroutine can stop itself and pass control to the scheduler."
Thanks. I would also add that a coroutine cannot be stopped from the outside. Will this be ok?

Rob Landers

290 days ago
On Sat, Nov 15, 2025, at 15:41, Edmond Dantes wrote:
> Hello. > > > Based on the conversation so far, I’d imagine the list to look something like: > > Yes, that’s absolutely correct. When a programmer uses an operation > that would normally block the entire thread, control is handed over to > the Scheduler instead. > The suspend function is called inside all of these operations.
I think that "normally" is doing a lot of work here. `fwrite()` can block, but often doesn’t. `file_get_contents()` is usually instant for local files but can take seconds on NFS or with an HTTP URL. An `array_map()` *always* blocks the thread but should *never* suspend. Without very clear rules, it becomes impossible to reason about what’ll suspend and what won’t.
> > > If that’s the intended model, it’d help to have that spelled out directly; it makes it immediately clear which functions can or will suspend and prevents surprises. > > In the Async implementation, it will be specified which functions are supported.
This is exactly the kind of thing that needs to be in the RFC itself. Relying on "the implementation will document it" creates an unstable contract. Even something simple like: - if it can perform network IO - if it can perform file/stream IO - if it can sleep or wait on timers - if it awaits a `FutureLike` - if it calls `suspend()` This would then create a stable baseline and require an RFC to change the rules, forcing people to think through BC breakages and ecosystem impact.
> > > I also think the RFC needs at least minimal wording about scheduler guarantees, even if the details are implementation-specific. > The Scheduler guarantees that a coroutine will be invoked if it is in the queue.
That’s not quite enough. The order really matters. Different schedulers produce different observable results. For example: function step(string $name, string $msg) { echo "$name: $msg\n"; suspend(); } spawn(function() { step("A", "1"); step("A", "2"); step("A", "3"); }); spawn(function() { step("B", "1"); step("B", "2"); step("B", "3"); }); spawn(function() { step("C", "1"); step("C", "2"); step("C", "3"); }); Under different scheduling strategies you get different, but stable patterns. Consider FIFO or round-robin, run-to-suspend: A: 1 B: 1 C: 1 A: 2 B: 2 Cl: 2 A: 3 B: 3 C: 3 But with a stack-like or LIFO strategy, running-to-suspend: A: 1 B: 1 C: 1 C: 2 C: 3 B: 2 B: 3 A: 2 A: 3 Both are valid, but are important to *know* which one is implemented, and if someone wants to replace the scheduler, they also need to ensure they guarantee this behaviour.
> > > For example, is the scheduler run-to-suspend? FIFO or round-robin wakeup? And non-preemptive behaviour only appears here in the thread. It isn’t mentioned in the RFC itself. > > In Go, for example, when it was still cooperative, these details were > also not part of any public contract. The only guarantee Go provided > was that a coroutine would not be interrupted arbitrarily. The same > applies to this RFC: coroutines are interrupted only at designated > suspension points. > However, neither Go nor any other language exposes the internal > details of the Scheduler as part of a public contract, because those > details may change without notice.
Go did document these details during its cooperative era, including exactly where goroutines might yield. Unfortunately, I can’t find a link to documentation that old. I did come across the old design docs that might shed some light on how things worked back then: https://go.dev/wiki/DesignDocuments The key point is that Go made cooperative scheduling predictable enough that developers could write performant code without guessing.
> > > That’s important for people writing long, CPU-bound loops, since nothing will interrupt them unless they explicitly yield. > Hypothetically, in the future it may become possible to interrupt > loops, just like Go eventually did. This would likely require an > additional RFC. PHP does have the ability to interrupt a loop at any > point, but most likely only for terminating execution. > This RFC does nothing of the sort.
My concern isn’t the lack of loop preemption. My concern is that the RFC never *says* CPU loops *don’t yield.* If it isn’t stated explicitly, it won’t be documented, and users will discover it the hard way. That’s exactly the sort of footgun we should avoid at the language level.
> > Lastly, cancellation during a syscall is still unclear. If a coroutine is cancelled while something like fwrite() or a DB write is in progress, what should happen? > > Does fwrite() still return the number of bytes written? Does it throw? For write-operations in particular, this affects whether applications can maintain a consistent state. > > If the write operation is interrupted, the function will return an > error according to its contract. In this case, it will return false.
`fwrite()` almost never returns `false`, it returns "bytes written OR false". Partial successful writes are normal and extremely common. So, cancellation *does* change the behaviour unless this is spelled out very carefully so calling code can recover appropriately.
> > > Clarifying these points would really help people understand how to reason about concurrency with this API. > > This is described in the document.
I may be missing something, but I don’t see this spelled out anywhere in the RFC.
> There is, of course, a nuance regarding extended error descriptions, > but at the moment no such changes are planned.
That’s fine, but then do you expect the RFC to pass as-is? Right now, without suspension rules, scheduler guarantees, defined syscall-cancellation semantics, it’s tough to evaluate the correctness and performance implications. Leaving some of the most important aspects as an "implementation detail" seems like asking for trouble. — Rob

Jakub Zelenka

289 days ago
Hi, On Sat, Nov 15, 2025 at 8:56 PM Rob Landers <rob@bottled.codes> wrote:
> On Sat, Nov 15, 2025, at 15:41, Edmond Dantes wrote: > > Hello. > > > Based on the conversation so far, I’d imagine the list to look something > like: > > Yes, that’s absolutely correct. When a programmer uses an operation > that would normally block the entire thread, control is handed over to > the Scheduler instead. > The suspend function is called inside all of these operations. > > > I think that "normally" is doing a lot of work here. fwrite() can block, > but often doesn’t. file_get_contents() is usually instant for local files > but can take seconds on NFS or with an HTTP URL. An array_map() *always* > blocks the thread but should *never* suspend. > > Without very clear rules, it becomes impossible to reason about what’ll > suspend and what won’t. > > > > If that’s the intended model, it’d help to have that spelled out > directly; it makes it immediately clear which functions can or will suspend > and prevents surprises. > > In the Async implementation, it will be specified which functions are > supported. > > > This is exactly the kind of thing that needs to be in the RFC itself. > Relying on "the implementation will document it" creates an unstable > contract. > > Even something simple like: > > - if it can perform network IO > - if it can perform file/stream IO > - if it can sleep or wait on timers >
None of the above is part is this RFC so why is this being discussed. Any of the changes to stream layer and extensions will require special RFC and mainly clean implementation. We will need to carefully consider where the suspension is going to be done. I think if there are parts of the RFC that mention IO, it should be removed here. I think this RFC should also remove any mention of reactor as it's irrelevant for this. Kind regards, Jakub

Rob Landers

289 days ago
On Sat, Nov 15, 2025, at 22:17, Jakub Zelenka wrote:
> Hi, > > On Sat, Nov 15, 2025 at 8:56 PM Rob Landers <rob@bottled.codes> wrote: >> __ >> On Sat, Nov 15, 2025, at 15:41, Edmond Dantes wrote: >>> Hello. >>> >>> > Based on the conversation so far, I’d imagine the list to look something like: >>> >>> Yes, that’s absolutely correct. When a programmer uses an operation >>> that would normally block the entire thread, control is handed over to >>> the Scheduler instead. >>> The suspend function is called inside all of these operations. >> >> I think that "normally" is doing a lot of work here. `fwrite()` can block, but often doesn’t. `file_get_contents()` is usually instant for local files but can take seconds on NFS or with an HTTP URL. An `array_map()` *always* blocks the thread but should *never* suspend. >> >> Without very clear rules, it becomes impossible to reason about what’ll suspend and what won’t. >> >>> >>> > If that’s the intended model, it’d help to have that spelled out directly; it makes it immediately clear which functions can or will suspend and prevents surprises. >>> >>> In the Async implementation, it will be specified which functions are supported. >> >> This is exactly the kind of thing that needs to be in the RFC itself. Relying on "the implementation will document it" creates an unstable contract. >> >> Even something simple like: >> >> - if it can perform network IO >> - if it can perform file/stream IO >> - if it can sleep or wait on timers > > None of the above is part is this RFC so why is this being discussed. Any of the changes to stream layer and extensions will require special RFC and mainly clean implementation. We will need to carefully consider where the suspension is going to be done.
My point is that it *should* be a part of the RFC. — Rob

Jakub Zelenka

289 days ago
On Sat, Nov 15, 2025 at 10:19 PM Rob Landers <rob@bottled.codes> wrote:
> > > On Sat, Nov 15, 2025, at 22:17, Jakub Zelenka wrote: > > Hi, > > On Sat, Nov 15, 2025 at 8:56 PM Rob Landers <rob@bottled.codes> wrote: > > > On Sat, Nov 15, 2025, at 15:41, Edmond Dantes wrote: > > Hello. > > > Based on the conversation so far, I’d imagine the list to look something > like: > > Yes, that’s absolutely correct. When a programmer uses an operation > that would normally block the entire thread, control is handed over to > the Scheduler instead. > The suspend function is called inside all of these operations. > > > I think that "normally" is doing a lot of work here. fwrite() can block, > but often doesn’t. file_get_contents() is usually instant for local files > but can take seconds on NFS or with an HTTP URL. An array_map() *always* > blocks the thread but should *never* suspend. > > Without very clear rules, it becomes impossible to reason about what’ll > suspend and what won’t. > > > > If that’s the intended model, it’d help to have that spelled out > directly; it makes it immediately clear which functions can or will suspend > and prevents surprises. > > In the Async implementation, it will be specified which functions are > supported. > > > This is exactly the kind of thing that needs to be in the RFC itself. > Relying on "the implementation will document it" creates an unstable > contract. > > Even something simple like: > > - if it can perform network IO > - if it can perform file/stream IO > - if it can sleep or wait on timers > > > None of the above is part is this RFC so why is this being discussed. Any > of the changes to stream layer and extensions will require special RFC and > mainly clean implementation. We will need to carefully consider where the > suspension is going to be done. > > > My point is that it *should* be a part of the RFC. >
But this is hard to know exactly. Also there will be always 3rd extensions that can block so we will need to do it piece by piece. You can just take it that ideally everything that can block would be suspendable . The first candidate is surely stream internall poll that is used for stream IO in various places and could handles most suspensions including in mysqlnd. Then curl and sockets would be probably added. There are various other bits already present in Edmonds PoC but we will need to consider them one by one. In other words, we can't really know that until we have some base pieces merged (this RFC) and there is acceptable implementation that can be merged for those parts. Kind regards, Jakub

Edmond Dantes

289 days ago
Hello.
> An array_map() always blocks the thread but should never suspend.
This function is not related to this discussion or to the RFC.
> Without very clear rules, it becomes impossible to reason about what’ll suspend and what won’t.
As I mentioned earlier, this RFC clearly defines the rules for integrating functions. The functions themselves will be documented.
> That’s not quite enough. The order really matters. Different schedulers produce different observable results.
No modern language guarantees a fixed execution order of coroutines. Go, Kotlin, Python, JavaScript, C#, Rust. All only guarantee order when explicit synchronization is used. Everything else is an implementation detail of the scheduler, and user code must not rely on it. (Because concurrency naturally has many valid execution paths, not one. Forcing a single fixed order is impossible without adding heavy synchronization everywhere, which destroys performance and breaks the concurrency model.)
> Unfortunately, I can’t find a link to documentation that old.
no one can
> I may be missing something, but I don’t see this spelled out anywhere in the RFC.
What exactly were you unable to find?
> Right now, without suspension rules, scheduler guarantees, defined syscall-cancellation semantics, it’s tough to evaluate the correctness and performance implications. > Leaving some of the most important aspects as an "implementation detail" seems like asking for trouble.
I’m sorry, but it’s difficult for me to understand what this is referring to.

John Bafford

289 days ago
On Nov 15, 2025, at 16:20, Edmond Dantes <edmond.ht@gmail.com> wrote:
> > Hello. > >> An array_map() always blocks the thread but should never suspend. > This function is not related to this discussion or to the RFC.
function writeData() { return array_map(function($elt) { [$path, $content] = $elt; return [$path, file_put_contents($path, $content)]; //POSSIBLE SUSPENSION POINT }, $this->data); } Now this array_map function potentially suspends. As of course does this writeData(). And whatever calls this writeData(). And so on up the stack. _Any_ function that calls any other function might have a hidden suspension point. And as Rob pointed out, any hook might also have a hidden suspension point, so you can't even trust code that doesn't look like it calls functions. And even if there are no hooks, __get(), __set(), etc are also there to ruin your day.
>> To provide an explicit example for this, code that fits this pattern is going to be problematic > > Why is this considered a problem if this behavior is part of the > language’s contract?
Because this RFC *changes the contract* out from every line of php code ever written. Code that used to be strictly synchronous now has async suspension points it didn't ask for.
>> $this->data can be changed out from under writeData(), which leads to unexpected behavior. > > So the developer must intentionally create two different coroutines. > Intentionally pass them the same object. > Intentionally write this code. > And the behavior is called “unexpected”? :)
Yes. To repeat, a core premise of this RFC is:
> • From a PHP developer's perspective, the main value of this implementation is that they DO NOT NEED to change existing code (or if changes are required, they should be minimal) to enable concurrency. Unlike explicit async models, this approach lets developers reuse existing synchronous code inside coroutines without modification. > • Code that was originally written and intended to run outside of a Coroutine must work EXACTLY THE SAME inside a Coroutine without modifications.
With this, the RFC implies that I should be able to take my synchronous PHP code and run it in a coroutine with other synchronous PHP code, and it will all just work. But obviously that won't work. I understand that a different interpretation of this wording is, "well, that code does exactly what it did before, just that with coroutines, that happens to be broken". I could maybe squint and grunt disapprovingly about it being "technically correct", except "has a suspension point" is *definitely not* exactly how it worked before.
> The changes described in the RFC refer to the algorithm for handling > I/O functions in blocking mode. And of course these words assume that > we haven’t lost our minds and understand that you cannot write > completely different message sequences to the same socket at the same > time. In practice, changes are of course sometimes necessary, but > throughout my entire experience working with coroutines, I should note > that I have never once run into the example you mentioned. Even when > adapting older projects. And do you know why? Because the first thing > we refactored in the old code was the places with shared variables.
Well, but that's not what my example was doing. My example was taking a set of (filename, content) pairs and writing them to individual files. But it was doing it in an async-unsafe way (because that has never before been a consideration), and so its data source can be corrupted _while it is executing_. The async problems in my example might be glaringly obvious (for people skilled in the art), but many other async issues are much more subtle, such as Rob described in his follow-on email to mine. For people who have never had the pleasure of working with async code before, "the world changed out from under me" is not a scenario you're accustomed to thinking about.
> But in PHP, colored functions are inconvenient. Overloading I/O > functions does not lead to serious errors that make developers suffer; > on the contrary, it saves time and gives the language more > flexibility.
Of course colored functions are inconvenient. But it's necessary or else you open to a whole class of easily avoidable problems. _Those_ problems are _much_ more inconvenient than colored functions. Code that was written to be synchronous should remain synchronous unless it is explicitly modified to be asynchronous.
> A developer should strive to minimize asynchronous code in a project. > The less of it there is, the better. Asynchronous code is evil. An > anti-pattern. A high-complexity zone. But if a developer chooses to > use asynchronous code, they shouldn’t act like they’re three years old > and seeing a computer for the first time. Definitely not. This > technology requires steady, capable hands :)
What this says to me is, "Here's a foot-gun. Please use it responsibly." Now, I definitely want to have advanced features available for when they're needed. But that said, It would be great if we can avoid introducing new foot-guns, especially when we have the knowledge and experience from other languages to draw on and do better. And when we do introduce new foot-guns, it's better if we can make them strategically-targeted sniper rifles instead of blunderbusses. If we allow for hidden async behavior, the entire system becomes impossible to reason about. Some library I'm using can cause an async race condition without me asking for it, and I can't know it doesn't unless I audit it and its interaction with my code. And we know from practice that enough people won't use async responsibly that it will make the language look bad. What will happen is a junior dev will decide (or be told) that some code is performing poorly and they will see a forum post that says "use coroutines" or "use async" and they will cargo-cult their way to a hidden problem because they didn't take into consideration the full problem space. Not to say that can't happen with explicit suspensions, but the ceremony of declaring you have a possible suspension at least gives a pointer of, "this is where there's a suspension; what happens if it does?". -John

Michael Morris

289 days ago
I've been reading this discussion from the peanut gallery. I don't have enough knowledge to really add anything to the conversation, but I do want to take a moment to thank everyone involved. Of the changes to PHP since 7, this promises to be some of the most significant. The care and time taken to get this right is greatly appreciated. To all participants of the thread, thank you.

Edmond Dantes

289 days ago
> Now this array_map function potentially suspends. As of course does this writeData(). And whatever calls this writeData(). And so on up the stack.
And the most important thing is that the __execution flow__ for this function will __not change__. Do you understand this? This is described very clearly in the RFC.
> Because this RFC *changes the contract* out from every line of php code ever written. Code that used to be strictly synchronous now has async suspension points it didn't ask for.
It’s not the RFC that changes the contract. It’s asynchronous programming that changes the contract. And that lies outside the scope of this RFC, because once a developer starts using asynchronous programming, they must accept the global contracts of asynchronous execution. And this RFC does not deny that.
> To repeat, a core premise of this RFC is:
Sorry, but I’m not going to repeat points that were ignored. My goal is to help you understand the text of this RFC. I have no intention of banging my head against a wall.
> With this, the RFC implies that I should be able to take my synchronous PHP code and run it in a coroutine with other synchronous PHP code, and it will all just work. But obviously that won't work.
For most cases that’s exactly how it works. In human language, there is no definition that cannot be twisted and misinterpreted. Why elevate this idea to an absolute while ignoring common sense? I have no idea. What’s the purpose of this discussion? It seemed to me that the purpose of the discussion was a professional conversation, not playing with meanings.
> I understand that a different interpretation of this wording is, "well, that code does exactly what it did before, just that with coroutines, that happens to be broken".
I have no idea what you’re talking about.
> Of course colored functions are inconvenient. But it's necessary or else you open to a whole class of easily avoidable problems. _Those_ problems are _much_ more inconvenient than colored functions.
I already discussed this topic back in March, and I can briefly summarize: the absence of colored functions in PHP is the only viable way to implement async.
> Code that was written to be synchronous should remain synchronous unless it is explicitly modified to be asynchronous.
Practice has proven the opposite.
> What this says to me is, "Here's a foot-gun. Please use it responsibly."
A footgun implies something hidden or implicit. Asynchronous programming requires the developer to handle memory correctly explicitly. This is equally true for all programming languages. It does not relate specifically to this RFC.
> especially when we have the knowledge and experience from other languages to draw on and do better
How do you plan to draw on the experience of other languages if you’re ignoring the experience of asynchronous PHP that has existed for many years? If you truly wanted to rely on such experience, there wouldn’t be this discussion.

John Bafford

290 days ago
Hi Rob, Edmond,
> On Nov 15, 2025, at 06:37, Rob Landers <rob@bottled.codes> wrote: > > I have concerns about the clarity of when suspension occurs in this RFC. > > The RFC states as a core goal: > > "Code that was originally written and intended to run outside of a Coroutine must work EXACTLY THE SAME inside a Coroutine without modifications." > > And: > > "A PHP developer should not have to think about how Coroutine switch and should not need to manage their switching—except in special cases where they consciously choose to intervene in this logic." > > [...] > > With explicit async/await ("coloured functions"), developers know exactly where suspension can occur. This RFC’s implicit model seems convenient, but without clear rules about suspension points, I’m unclear how developers can write correct concurrent code or reason about performance. > > Could the RFC clarify the rules for when automatic suspension occurs versus when manual suspend() calls are required? Is this RFC following Go’s model where suspension timing is an implementation detail developers shouldn’t rely on? If so, that should be stated explicitly. Keep in mind that Go didn’t start that way and took nearly a decade to get there. Earlier versions of Go explicitly stated where suspensions were. > > — Rob
To provide an explicit example for this, code that fits this pattern is going to be problematic: function writeData() { $count = count($this->data); for($x = 0; $x < $count; $x++) { [$path, $content] = $this->data[$x]; file_put_contents($path, $content); } $this->data = []; } While there are better ways to write this function, in normal PHP code, there's no problem here. But if file_put_contents() can block and cause a different coroutine to run, $this->data can be changed out from under writeData(), which leads to unexpected behavior. (e.g. $this->data changes length, and now writeData() no longer covers all of it; or it runs past the end of the array and errors; or doesn't see there's a change and loses it when it clears the data). Now, yes, the programmer would have to do something to cause there to be two coroutines running in the first place. But if _this_ code was correct when "originally written and intended to run outside of a Coroutine", and with no changes is incorrect when run inside a coroutine, one can only say that it is working "exactly the same" with coroutines by ignoring that it is now wrong. Suspension points, whether explicit or hidden, allow for the entire rest of the world to change out from under the caller. The only way for non-async-aware code to operate safely is for suspension to be explicit (which, of course, means the code now must be async-aware). There is no way in general for code written without coroutines or async suspensions in mind to work correctly if it can be suspended. -John

Edmond Dantes

290 days ago
> To provide an explicit example for this, code that fits this pattern is going to be problematic
Why is this considered a problem if this behavior is part of the language’s contract? Exactly the same way as in Go for example, this is also part of the contract between the language and the programmer.
> $this->data can be changed out from under writeData(), which leads to unexpected behavior.
So the developer must intentionally create two different coroutines. Intentionally pass them the same object. Intentionally write this code. And the behavior is called “unexpected”? :)
> that it is working "exactly the same" with coroutines by ignoring that it is now wrong
I understand that a word written by one person can be interpreted however another person feels like. Language is not a reliable carrier of information, so people must take context into account to extract useful information with minimal distortion. The changes described in the RFC refer to the algorithm for handling I/O functions in blocking mode. And of course these words assume that we haven’t lost our minds and understand that you cannot write completely different message sequences to the same socket at the same time. In practice, changes are of course sometimes necessary, but throughout my entire experience working with coroutines, I should note that I have never once run into the example you mentioned. Even when adapting older projects. And do you know why? Because the first thing we refactored in the old code was the places with shared variables.
> There is no way in general for code written without coroutines or async suspensions in mind to work correctly if it can be suspended.
Agreed. A developer must understand that potentially any function can interrupt execution. This is a consequence of transparent asynchrony. It is both its strength and its weakness. I will repeat it again: not some specific function, but almost ANY function. Because under transparent asynchrony you can use suspend() inside any function. This does not negate the fact that documentation should list all functions that switch context, but a certain coding style encourages this way of thinking. Modern programming languages strive for clarity and cleanliness. In other words, colored functions provide code clarity and prevent errors caused by misunderstanding what a function does. Critics of colored functions criticize them precisely for what is actually their strength, not their weakness. Color is an advantage. But in PHP, colored functions are inconvenient. Overloading I/O functions does not lead to serious errors that make developers suffer; on the contrary, it saves time and gives the language more flexibility. I can explain why. The amount of code that works with sockets in PHP is generally several times smaller than the code that works with databases. In other words, the modules where such errors could occur are simply not that many. They do exist. library clients... but compared to all other code, there are far fewer of them. And as it turns out, refactoring them for coroutines requires very few changes. Especially if the code was already well-written with best practices in mind, then it will most likely work excellently with coroutines with minimal adjustments. How did we refactor old code for coroutines? 1. We took the modules that had global state. There were not many of them. 2. We used a Context, which is essential, and moved the global state into the context. I don’t remember exactly how many thousands of lines of code there were, but definitely more than 20,000. But why anyone would intentionally pass the same object to different coroutines and then complain that the code broke. I have no idea who would need that. :) A developer should strive to minimize asynchronous code in a project. The less of it there is, the better. Asynchronous code is evil. An anti-pattern. A high-complexity zone. But if a developer chooses to use asynchronous code, they shouldn’t act like they’re three years old and seeing a computer for the first time. Definitely not. This technology requires steady, capable hands :) Best Regards, Ed.

Rob Landers

290 days ago
On Sat, Nov 15, 2025, at 18:22, Edmond Dantes wrote:
> > To provide an explicit example for this, code that fits this pattern is going to be problematic > > Why is this considered a problem if this behavior is part of the > language’s contract? > Exactly the same way as in Go for example, this is also part of the > contract between the language and the programmer.
One of the stated goals of the RFC: *Code that was originally written and intended to run outside of a Coroutine must work EXACTLY THE SAME inside a Coroutine without modifications.* The examples you give here seem to contradict that. You are now saying that developers *must* refactor shared state, must avoid passing objects to multiple coroutines, and must adopt a certain programming style to avoid breaking existing code. That’s the opposite of "works exactly the same without modification".
> > $this->data can be changed out from under writeData(), which leads to unexpected behavior. > > So the developer must intentionally create two different coroutines. > Intentionally pass them the same object. > Intentionally write this code. > And the behavior is called “unexpected”? :)
The original claim of the RFC is that code *not written with coroutines in mind* should still behave the same inside them. If any function can suspend at arbitrary points, the ordinary synchronous assumptions, including read/modify/write patterns on properties, no longer hold. Whether that pattern is good style or not doesn’t change the fact that the behaviour is different once asynchrony is introduced.
> A developer must understand that potentially any function can > interrupt execution. This is a consequence of transparent asynchrony.
This is also a direct tension with another major goal: *A PHP developer should not have to think about how Coroutine switch and should not need to manage their switching—except in special cases where they consciously choose to intervene in this logic.* If any function can suspend, then developers MUST reason about all the usual concurrency hazards: torn writes, interleaving, race conditions, and the entire class of bugs that coloured function models prevent. That absolutely counts as "thinking about coroutine switching".
> It is both its strength and its weakness. I will repeat it again: not > some specific function, but almost ANY function. Because under > transparent asynchrony you can use suspend() inside any function. This > does not negate the fact that documentation should list all functions > that switch context, but a certain coding style encourages this way of > thinking.
These statements also seem to go against another goal of the RFC: *A PHP developer should not have to think about how Coroutine switch and should not need to manage their switching—except in special cases where they consciously choose to intervene in this logic.*
> How did we refactor old code for coroutines? > > 1. We took the modules that had global state. There were not many of them. > 2. We used a Context, which is essential, and moved the global state > into the context. > [snip] > But why anyone would intentionally pass the same object to different > coroutines and then complain that the code broke. I have no idea who > would need that. :)
Existing PHP codes does this today without issue. Many libraries, parsers, database clients, stream decorators, in-memory caches, DTOs, middleware chains ... are built around shared mutable objects. That style is extremely common in PHP, and today it’s perfectly fine to share these things. Saying "just refactor all your shared-state-code" seems to contradict the goals given in the RFC.
> A developer should strive to minimize asynchronous code in a project. > The less of it there is, the better. Asynchronous code is evil. An > anti-pattern. A high-complexity zone. But if a developer chooses to > use asynchronous code, they shouldn’t act like they’re three years old > and seeing a computer for the first time. Definitely not. This > technology requires steady, capable hands :) > > Best Regards, Ed.
Right now, today, PHP has almost zero async code in the ecosystem. If the position of the RFC is that transparent asynchrony is inherently dangerous, requires careful discipline, breaks common patterns, and requires refactoring shared state, then it isn’t clear how the central value proposition "existing code works unchanged" is meant to hold. This is why the semantics need to be written down explicitly, not left to implication or the experience of those who already work with coroutines. — Rob

Edmond Dantes

289 days ago
> The examples you give here seem to contradict that. You are now saying that developers must refactor shared state, must avoid passing objects to multiple coroutines, > and must adopt a certain programming style to avoid breaking existing code. That’s the opposite of "works exactly the same without modification".
I provided an explanation in my earlier messages.
> Right now, today, PHP has almost zero async code in the ecosystem.
That is not accurate. PHP already has a significant amount of async-style code in the ecosystem: Amphp, ReactPHP, Swoole, Swow, and multiple async HTTP clients, database drivers, and event-loop libraries. The ecosystem is not “almost zero”; it’s simply fragmented across several implementations.
> If the position of the RFC is that transparent asynchrony
My comment was about programming languages in general.

Rob Landers

289 days ago
On Sat, Nov 15, 2025, at 17:20, John Bafford wrote:
> Hi Rob, Edmond, > > > On Nov 15, 2025, at 06:37, Rob Landers <rob@bottled.codes> wrote: > > > > I have concerns about the clarity of when suspension occurs in this RFC. > > > > The RFC states as a core goal: > > > > "Code that was originally written and intended to run outside of a Coroutine must work EXACTLY THE SAME inside a Coroutine without modifications." > > > > And: > > > > "A PHP developer should not have to think about how Coroutine switch and should not need to manage their switching—except in special cases where they consciously choose to intervene in this logic." > > > > [...] > > > > With explicit async/await ("coloured functions"), developers know exactly where suspension can occur. This RFC’s implicit model seems convenient, but without clear rules about suspension points, I’m unclear how developers can write correct concurrent code or reason about performance. > > > > Could the RFC clarify the rules for when automatic suspension occurs versus when manual suspend() calls are required? Is this RFC following Go’s model where suspension timing is an implementation detail developers shouldn’t rely on? If so, that should be stated explicitly. Keep in mind that Go didn’t start that way and took nearly a decade to get there. Earlier versions of Go explicitly stated where suspensions were. > > > > — Rob > > To provide an explicit example for this, code that fits this pattern is going to be problematic: > > function writeData() { > $count = count($this->data); > for($x = 0; $x < $count; $x++) { > [$path, $content] = $this->data[$x]; > file_put_contents($path, $content); > } > $this->data = []; > } > > While there are better ways to write this function, in normal PHP code, there's no problem here. But if file_put_contents() can block and cause a different coroutine to run, $this->data can be changed out from under writeData(), which leads to unexpected behavior. (e.g. $this->data changes length, and now writeData() no longer covers all of it; or it runs past the end of the array and errors; or doesn't see there's a change and loses it when it clears the data). > > Now, yes, the programmer would have to do something to cause there to be two coroutines running in the first place. But if _this_ code was correct when "originally written and intended to run outside of a Coroutine", and with no changes is incorrect when run inside a coroutine, one can only say that it is working "exactly the same" with coroutines by ignoring that it is now wrong. > > Suspension points, whether explicit or hidden, allow for the entire rest of the world to change out from under the caller. The only way for non-async-aware code to operate safely is for suspension to be explicit (which, of course, means the code now must be async-aware). There is no way in general for code written without coroutines or async suspensions in mind to work correctly if it can be suspended. > > -John
I should have put all these emails combined into a single email ... but here we are. John’s example captures the core issue, and I want to take a moment and expand on it from a different angle. My concern with implicit suspensions isn’t theoretical. It’s exactly why nearly every modern language abandoned this model. Transparent, implicit suspension means that *any* line of code can become an interleaving point. That makes a large class of patterns, which are perfectly safe in synchronous PHP today, unsafe the moment they run inside a coroutine. A few concrete examples: With property hooks and implicit suspension, event this becomes unsafe: $this->counter++; A suspension can happen between the read and the write. Another coroutine can mutate the counter in between. The programmer did nothing wrong; it's just a hazard introduced by invisible suspension. And consider this can break invariants: $this->balance -= $amount; $this->ledger->writeEntry($this->id, -$amount); If the first line suspends, the balance can be changed somewhere else before the ledger entry is written (which breaks an invariant that the balance is a reflection of the ledger). With transparent async, it's suddenly a race condition. Then you can have time pass invisibly: if(!$cache->has($key)) { $cache->set($key, $value); } If has() suspends, anything can happen to that cache key before the set. The invariant becomes incorrect. Implicit suspension allows any function to be re-entered before it returns. That can lead to partially updated objects, state machines appearing to skip states, "method called twice before return" bugs, double writes, and re-entrant callbacks being invoked with inconsistent state. The bugs are extremely challenging to debug because the programmer never actually wrote any async code. I’ve had the "pleasure" of working on Fiber frameworks that use raw fibers (no async/await you get from React/Amp, though I’ve worked with those pretty extensively as well). These are the bugs you run into all the time, where you sometimes have to literally put a suspension in a seemingly random place to fix a bug. Implicit async blurs one of the most important boundaries in software design: "this code cannot be interrupted" vs "this code can be interrupted". - JavaScript moved from implicit async -> promises -> async/await - Python moved from callbacks/greenlets -> async/await - Ruby moved from fibers -> explicit schedulers - Go eventually added true preemption Even the creators of Fibers eventually wrote async/await on top of them, because implicit async is broken and coloured functions close off entire classes of bugs and make reasoning possible again. I understand the desire for "transparent async" but once a language allows suspension at arbitrary points, the language can no longer promise invariants, atomic sequences, non-reentrancy, predictable control flow, or even correctness, in-general. — Rob

Edmond Dantes

289 days ago
> With property hooks and implicit suspension, event this becomes unsafe: > A suspension can happen between the read and the write. Another coroutine can mutate the counter in between. The programmer did nothing wrong; it's just a hazard introduced by invisible suspension.
The risk of a variable being modified by different coroutines does not depend on the transparency model. This effect is possible in both implementations. Even if a setter triggers a suspension, it does not affect the logical execution flow. Therefore, no danger arises. The difference between the transparent model and the explicit one lies in other aspects. It seems this discussion took place in March of this year.