Re: PHP True Async RFC Stage 5

From: Date: Sat, 15 Nov 2025 21:11:21 +0000
Subject: Re: PHP True Async RFC Stage 5
References: 1 2 3 4  Groups: php.internals 
Request: Send a blank email to internals+get-129238@lists.php.net to get a copy of this message
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


Thread (71 messages)

« previous php.internals (#129238) next »