Re: Re: PHP True Async RFC

From: Date: Sun, 09 Mar 2025 13:17:43 +0000
Subject: Re: Re: PHP True Async RFC
References: 1 2 3 4  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message
On 08/03/2025 20:22, Edmond Dantes wrote:
For coroutines to work, a Scheduler must be started. There can be only one Scheduler per OS thread. That means creating a new async task does not create a new Scheduler. Apparently, async {} in the examples above is the entry point for the Scheduler.
I've been pondering this, and I think talking about "starting" or "initialising" the Scheduler is slightly misleading, because it implies that the Scheduler is something that "happens over there". It sounds like we'd be writing this: // No scheduler running, this is probably an error Async\runOnScheduler( something(...) ); Async\startScheduler(); // Great, now it's running... Async\runonScheduler( something(...) ); // If we can start it, we can stop it I guess? Async\stopScheduler(); But that's not we're talking about. As the RFC says:
Once the Scheduler is activated, it will take control of the Null-Fiber context, and execution within it will pause until all Fibers, all microtasks, and all event loop events have been processed.
The actual flow in the RFC is like this: // This is queued somewhere special, ready for a scheduler to pick it up later Async\enqueueForScheduler( something(...) ); // Only now does anything actually run Async\runSchedulerUntilQueueEmpty(); // At this point, the scheduler isn't running any more // If we add to the queue now, it won't run unless we run another scheduler Async\enqueueForScheduler( something(...) ); Pondering this, I think one of the things we've been missing is what Unix[-like] systems call "process 0". I'm not an expert, so may get details wrong, but my understanding is that if you had a single-tasking OS, and used it to bootstrap a Unix[-like] system, it would look something like this: 1. You would replace the currently running single process with the new kernel / scheduler process 2. That scheduler would always start with exactly one process in the queue, traditionally called "init" 3. The scheduler would hand control to process 0 (because it's the only thing in the queue), and that process would be responsible for starting all the other processes in the system: TTYs and login prompts, network daemons, etc I think the same thing applies to scheduling coroutines: we want the Scheduler to take over the "null fiber", but in order to be useful, it needs something in its queue. So I propose we have a similar "coroutine zero" [name for illustration only]: // No scheduler running, this is an error Async\runOnScheduler( something(...) ); Async\runScheduler(
    coroutine_zero: something(...);
); // At this point, the scheduler isn't running any more It's then the responsibility of "coroutine 0", here the function "something", to schedule what's actually wanted, like a network listener, or a worker pool reading from a queue, etc. At that point, the relationship to a block syntax perhaps becomes clearer: async {    spawn start_network_listener(); } is roughly (ignoring the difference between a code block and a closure) sugar for: Async\runScheduler(
    coroutine_zero: function() {
       spawn start_network_listener();
   } ); That leaves the question of whether it would ever make sense to nest those blocks (indirectly, e.g. something() itself contains an async{} block, or calls something else which does). I guess in our analogy, nested blocks could be like running Containers within the currently running OS: they don't actually start a new Scheduler, but they mark a namespace of related coroutines, that can be treated specially in some way. Alternatively, it could simply be an error, like trying to run the kernel as a userland program. -- Rowan Tommins [IMSoP]

Thread (110 messages)

« previous php.internals (#126676) next »