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