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