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

Reply via email to