PHP True Async RFC

php.internals

Edmond Dantes

1 year ago
Good day, everyone. I hope you're doing well. I’d like to introduce a draft version of the RFC for the True Async component. https://wiki.php.net/rfc/true_async I believe this version is not perfect and requires analysis. And I strongly believe that things like this shouldn't be developed in isolation. So, if you think any important (or even minor) aspects have been overlooked, please bring them to attention. The draft status also highlights the fact that it includes doubts about the implementation and criticism. The main global issue I see is the lack of "future experience" regarding how this API will be used—another reason to bring it up for public discussion. Wishing you all a great day, and thank you for your feedback!

Rob Landers

1 year ago
On Sat, Mar 1, 2025, at 10:11, Edmond Dantes wrote:
> Good day, everyone. I hope you're doing well. > > I’d like to introduce a draft version of the RFC for the True Async component. > > https://wiki.php.net/rfc/true_async > > I believe this version is not perfect and requires analysis. And I strongly believe that things like this shouldn't be developed in isolation. So, if you think any important (or even minor) aspects have been overlooked, please bring them to attention. > > The draft status also highlights the fact that it includes doubts about the implementation and criticism. The main global issue I see is the lack of "future experience" regarding how this API will be used—another reason to bring it up for public discussion. > > Wishing you all a great day, and thank you for your feedback! >
FYI: once you introduce a draft RFC for discussion, the RFC should change status to "under discussion" per (4): https://wiki.php.net/rfc/howto — Rob

Edmond Dantes

1 year ago
> > FYI: once you introduce a draft RFC for discussion, the RFC should change > status to "under discussion" per (4): > >
It's done. Thank you. Ed.

Rowan Tommins [IMSoP]

1 year ago
On 01/03/2025 09:11, Edmond Dantes wrote:
> > Good day, everyone. I hope you're doing well. > > I’d like to introduce a draft version of the RFC for the True Async > component. > > https://wiki.php.net/rfc/true_async >
My reaction to this can be summed up as "this is huge!" By that I mean multiple things... First: PHP having native async support would be a huge step forward for the language. It's really exciting to see how this proposal develops. Second: it's clear you've put a huge amount of work into this, so a huge thank you for that, and I hope it is rewarded. Third: this is a huge proposal to digest. I wonder if there are ways it can be split into smaller pieces, so that we don't overlook details in one part because our focus is drawn to another. That might mean releasing a partial implementation this year, and more features next year; or it might just mean discussing and merging some core pieces first, then immediately following up with a series of feature RFCs, all targeting the same release. Fourth: design decisions here will have a huge impact on the language for years to come. We should spend plenty of time looking at experience from elsewhere - other languages, and existing third-party async implementations for PHP. This is closely related to the previous point, since expanding the current RFC with comparisons for every decision would make it impractically long. Fifth: this is a huge amount of new code - GitHub says 24 thousand lines of added code, although some of that is tests and documentation (which is great to see included!) We need to make sure there are enough people who understand the implementation to maintain that. Maybe we can try to tempt some of the core contributors to existing third-party libraries to spend some of their time on php-src instead. I realise I haven't actually given any concrete feedback on the proposal - I don't have any experience with other async implementations, and don't fully understand the concepts involved, so don't feel qualified to comment on the high-level design questions. I might have opinions on smaller design details (random example: RESOLVE, CANCEL, and TIMEOUT should be cases on an enum, not int constants) but see point 4: there's just too much here to discuss in that level of detail, and there are top-level decisions which should be our focus first. To re-iterate: this is really exciting, and thanks for getting it to this stage!
-- Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
> > First: PHP having native async support would be a huge step forward for > the language. It's really exciting to see how this proposal develops. > > Thank you for the kind words, it was awesome to read.
I wonder if there are ways it can be split into smaller pieces, so that
> we don't overlook details in one part because our focus is drawn to another.
I can suggest the following workflow: 1. *Approval of the core concept*: Changes affecting the language core. 2. *Decision on the low-level API*: Async\wait + Resume + microtask. Should it be exposed to PHP developers or not? (I don’t have a definitive answer). This is a crucial point that impacts 30-40% of the code. If the decision is made to hide this API, the code will need to be adjusted. Next, the RFC can be split into two parts: - *Low-level*: Basic PHP primitive functions + C API - *High-level*: Future, await, Channel, and maybe Pool. So the process would be: 3. Approval of the *Low-level RFC* 4. Approval of the *High-level RFC*. Step 4 depends on Step 3 in terms of implementation but is almost independent in terms of *semantics*. This means it can be discussed separately and more freely. Additionally, the *Low-level API* can be released independently, allowing PHP extensions to adopt concurrency earlier. As for function names, I really hope for your support in this matter because it's far from trivial. Thanks, Ed.

Rob Landers

1 year ago
On Sat, Mar 1, 2025, at 18:20, Rowan Tommins [IMSoP] wrote:
> On 01/03/2025 09:11, Edmond Dantes wrote: > > > > Good day, everyone. I hope you're doing well. > > > > I’d like to introduce a draft version of the RFC for the True Async > > component. > > > > https://wiki.php.net/rfc/true_async > > > > My reaction to this can be summed up as "this is huge!" By that I mean > multiple things... > > First: PHP having native async support would be a huge step forward for > the language. It's really exciting to see how this proposal develops. > > Second: it's clear you've put a huge amount of work into this, so a huge > thank you for that, and I hope it is rewarded. > > Third: this is a huge proposal to digest. I wonder if there are ways it > can be split into smaller pieces, so that we don't overlook details in > one part because our focus is drawn to another. That might mean > releasing a partial implementation this year, and more features next > year; or it might just mean discussing and merging some core pieces > first, then immediately following up with a series of feature RFCs, all > targeting the same release. > > Fourth: design decisions here will have a huge impact on the language > for years to come. We should spend plenty of time looking at experience > from elsewhere - other languages, and existing third-party async > implementations for PHP. This is closely related to the previous point, > since expanding the current RFC with comparisons for every decision > would make it impractically long. > > Fifth: this is a huge amount of new code - GitHub says 24 thousand lines > of added code, although some of that is tests and documentation (which > is great to see included!) We need to make sure there are enough people > who understand the implementation to maintain that. Maybe we can try to > tempt some of the core contributors to existing third-party libraries to > spend some of their time on php-src instead. > > > I realise I haven't actually given any concrete feedback on the proposal > - I don't have any experience with other async implementations, and > don't fully understand the concepts involved, so don't feel qualified to > comment on the high-level design questions. I might have opinions on > smaller design details (random example: RESOLVE, CANCEL, and TIMEOUT > should be cases on an enum, not int constants) but see point 4: there's > just too much here to discuss in that level of detail, and there are > top-level decisions which should be our focus first. > > To re-iterate: this is really exciting, and thanks for getting it to > this stage! > > -- > Rowan Tommins > [IMSoP] >
I second this, and as a long time user of amphp, go, and C#, I’d be coming into it with a specific mindset. My only thing so far is that it appears the scheduler cannot be replaced; at least, easily. I don’t know if we would do so over on FrankenPHP, but it would be interesting to replace the scheduler with something that utilized go-routines for true multi-threading. Whether that works or not, is a whole different can of worms. I’m compiling a deeper review, but that speaks more to the implementation than the spec. — Rob

Edmond Dantes

1 year ago
> but it would be interesting to replace the scheduler with something that > utilized go-routines for true multi-threading. Whether that works or not, > is a whole different can of worms. > > — Rob >
If the question is whether it is possible to interact with a PHP thread from another thread by sending an event to the Reactor, the answer is yes, it is possible. Moreover, from the PHP-land side, this could be a Channel. If the question is deeper — replacing the Scheduler with a Scheduler in another language or from a different ecosystem — then it is more likely possible than not, considering that the module itself is separated from the rest of the implementation. If you know a situation where this would be useful, then why not. For example, in cases of integration with a web server, we can just send a message through a channel from "server-thread" to "php-thread", and in a microtask written in C, for example, create Fibers to handle the request. This approach is used in Swoole. And this solution should be even slightly faster than in Swoole because the interaction will occur through memory copying within a single process. If memory copying is to be avoided, then the web server can be integrated directly into the Reactor, making the web server itself run as a microtask. Since the memory will be allocated immediately in the correct thread, there won’t even be a need to copy it, which in some cases might provide a performance boost. Or maybe not... Ed.

Rob Landers

1 year ago
On Sat, Mar 1, 2025, at 10:11, Edmond Dantes wrote:
> Good day, everyone. I hope you're doing well. > > I’d like to introduce a draft version of the RFC for the True Async component. > > https://wiki.php.net/rfc/true_async > > I believe this version is not perfect and requires analysis. And I strongly believe that things like this shouldn't be developed in isolation. So, if you think any important (or even minor) aspects have been overlooked, please bring them to attention. > > The draft status also highlights the fact that it includes doubts about the implementation and criticism. The main global issue I see is the lack of "future experience" regarding how this API will be used—another reason to bring it up for public discussion. > > Wishing you all a great day, and thank you for your feedback! >
Hey Edmond: I find this feature quite exciting! I've got some feedback so far, though most of it is for clarification or potential optimizations:
> A PHP developer *SHOULD NOT* make any assumptions about the order in which Fibers will be executed, as this order may change or be too complex to predict.
There should be a defined ordering (or at least, some guarantees). Being able to understand what things run in what order can help with understanding a complex system. Even if it is just a vague notion (user tasks are processed before events, or vice versa), it would still give developers more confidence in the code they write. You actually mention a bit of the order later (microtasks happen before fibers/events), so this sentence maybe doesn't make complete sense. Personally, I feel as though an async task should run as though it were a function call until it hits a suspension. This is mostly an optimization though (C# does this), but it could potentially reduce overhead of queueing a function that may never suspend (which you mention as a potential problem much later on): Async\run(*function*() { $fiber = Async\async(*function*() { sleep <http://www.php.net/sleep>(1); // this gets enqueued now return "Fiber completed!"; }); *// Execution is paused until the fiber completes* $result = Async\await($fiber); // immediately enter $fiber without queuing echo $result . "*\n*"; echo "Done!*\n*"; });
> Until it is activated, PHP code behaves as before: calls to blocking functions will block the execution thread and will not switch the *Fiber* context. Thus, code written without the *Scheduler* component will function exactly the same way, without side effects. This ensures backward compatibility.
I'm not sure I understand this. Won't php code behave exactly the same as it did before once enabling the scheduler? Will libraries written before this feature existed suddenly behave differently? Do we need to worry about the color of functions because it changes the behavior?
> `True Async` prohibits initializing the `Scheduler` twice.
How will a library take advantage of this feature if it cannot be certain the scheduler is running or not? Do I need to write a library for async and another version for non-async? Or do all the async functions with this feature work without the scheduler running, or do they throw a catchable error?
> This is crucial because the process may handle an OS signal that imposes a time limit on execution (for example, as Windows does).
Will this change the way os signals are handled then? Will it break compatibility if a library uses pcntl traps and I'm using true async traps too? Note there are several different ways (timeout) signals are handled in PHP -- so if (per-chance) the scheduler could always be running, maybe we can unify the way signals are handled in php.
> Code that uses *Resume* cannot rely on when exactly the *Fiber* will resume execution.
What if it never resumes at all? Will it call a finally block if it is try/catched or will execution just be abandoned? Is there some way to ensure cleanup of resources? It should probably mention this case and how abandoning execution works.
> If an exception is thrown inside a fiber and not handled, it will stop the Scheduler and be thrown at the point where `Async\launchScheduler()` is called.
The RFC doesn't mention the stack trace. Will it throw away any information about the inner exception?
> The *Graceful Shutdown* mode can also be triggered using the function:
What will calling `exit` or `die` do?
> A concurrent runtime allows handling requests using Fibers, where each Fiber can process its own request. In this case, storing request-associated data in global variables is no longer an option.
Why is this the case? Furthermore, if it inherits from the fiber that started its current fiber, won't using Resume/Notifier potentially cause problems when used manually? There are examples over the RFC using global variables in closures; so do these examples not actually work? Will sharing instances of objects in scope of the functions break things? For example: Async\run($obj->method1(...)); Async\run($obj->method2(...)); This is technically sharing global variables (well, global to that scope -- global is just a scope after all) -- so what happens here? Would it make sense to delegate this fiber-local storage to user-land libraries instead?
> Objects of the `Future` class are high-level patterns for handling deferred results.
By this point we have covered FiberHandle, Resume, and Contexts. Now we have Futures? Can we simplify this to just Futures? Why do we need all these different ways to handle execution?
> A channel is a primitive for message exchange between `Fibers`.
Why is there an `isEmpty` and `isNotEmpty` function? Wouldn't `!$channel->isEmpty()` suffice? It's also not clear what the value of most of these function is. For example: if ($chan->isFull()) { doSomething(); // suspends at some point inside? We may not know when we write the code. // chan is no longer full, or maybe it is -- who knows, but the original assumption entering this branch is no longer true. ... } Whether a channel is full or not is not really important, and if you rely on that information, this is usually an architectural smell (at least in other languages). Same thing with empty or writable, or many others of these functions. You basically just write to a channel and eventually (or not, which is a bug and causes a deadlock) something will read it. The entire point is to use channels to decouple async code, but most of the functions here allow for code to become strongly coupled. As for the single producer method, I am not sure why you would use this. I can see some upside for the built-in constraints (potentially in a dev-mode environment) but in a production system, single-producer bottlenecks are a real thing that can cause serious performance issues. This is usually something you explicitly want to avoid.
> In addition to the `send/receive` methods, which suspend the execution of a `Fiber`, the channel also provides non-blocking methods: `trySend`, `tryReceive`, and auxiliary explicit blocking methods: `waitUntilWritable` and `waitUntilReadable`.
It isn't clear what happens when `trySend` fails. Is this an error or does nothing? Thinking through it, there may be cases where `trySend` is valid, but more often than not, it is probably an antipattern. I cannot think of a valid reason for `tryReceive` and it's usage is most likely guaranteed to cause a deadlock in real code. For true multi-threaded applications, it makes more sense, but not for single-threaded concurrency like this. In other words, the following code is likely to be more robust, and not depend on execution order (which we are told at the beginning not to do): Async\run(*function*() { $channel = *new* Async\Channel(); $reader = Async\async(*function*() *use*($channel) { while ($data = $channel->read() && $data !== NULL) { echo "receive: *$data**\n*"; } }); for ($i = 0; $i < 4; $i++) { echo "send: event data *$i**\n*"; $data = $channel->send("event data *$i*"); } $reader->cancel(); // clean up our reader // or $channel->close(); // will receive NULL I believe? }); A `trySend` is still useful when you want to send a message but don't want to block if it is full. However, this is going to largely depend on how long is has been since the developer last suspended the current fiber, and nothing else -- thus it is probably an antipattern since it totally depends on the literal structure of the code, not the structure of the program -- if that makes sense.
> This means that `trapSignal` is not intended for “regular code” and should not be used “anywhere”.
Can you expand on what this means in the RFC? Why expose it if it shouldn't be used? ----- I didn't go into the low level api details yet -- this email is already pretty long. But I would suggest maybe thinking about how to unify Notifiers/Resume/FiberHandle/Future into a single thing. These things are pretty similar to one another (from a developer's standpoint) -- a way to continue execution, and they all offer a slightly different api. I also noticed that you seem to be relying heavily on the current implementation to define behavior. Ideally, the RFC should define behavior and the implementation implement that behavior as described in the RFC. In other words, the RFC is used as a reference point as to whether something is a bug or an enhancement in the future. There has been more than once where the list looks back at an old RFC to try and determine the intent for discovering if something is working as intended or a bug. RFCs are also used to write documentation, so the more detailed the RFC, the better the documentation will be for new users of PHP. — Rob

Edmond Dantes

1 year ago
> > There should be a defined ordering (or at least, some guarantees).
The execution order, which is *part of the contract*, is as follows: 1. Microtasks are executed first. 2. Then I/O events and OS signals are processed. 3. Then timer events are executed. 4. Only after that are fibers scheduled for execution. In the current implementation, fibers are stored in *a queue without priorities* (this is not a random choice). During one cycle period, only one fiber is taken from the queue. This results in the following code (I've removed unnecessary details): do { execute_microtasks_handler(); has_handles = execute_callbacks_handler(circular_buffer_is_not_empty(&ASYNC_G(deferred_resumes))); execute_microtasks_handler(); bool was_executed = execute_next_fiber_handler(); if (UNEXPECTED( false == has_handles && false == was_executed && zend_hash_num_elements(&ASYNC_G(fibers_state)) > 0 && circular_buffer_is_empty(&ASYNC_G(deferred_resumes)) && circular_buffer_is_empty(&ASYNC_G(microtasks)) && resolve_deadlocks() )) { break; } } while (zend_hash_num_elements(&ASYNC_G(fibers_state)) > 0 || circular_buffer_is_not_empty(&ASYNC_G(microtasks)) || reactor_loop_alive_fn() ); If we go into details, it is also noticeable that microtasks are executed twice - before and after event processing - because an event handler might enqueue a microtask, and the loop ensures that this code executes as early as possible. The contract for the execution order of microtasks and events is important because it must be considered when developing event handlers. The concurrent iterator relies on this rule. However, making assumptions about when a fiber will be executed is *not* part of the contract, if only because this algorithm can be changed at any moment. *// Execution is paused until the fiber completes* $result = Async\await(
> $fiber); // immediately enter $fiber without queuing
So is it possible to change the execution order and optimize context switches? Yes, there are ways to do this. However, it would require modifying the Fiber code, possibly in a significant way (I haven't explored this aspect in depth). But… let's consider whether this would be a good idea. We have a web server. A single thread is handling five requests. They all compete with each other because this is a typical application interacting with MySQL. In each Fiber, you send a query and wait for the result as quickly as possible. In what case should we create a new coroutine within a request handler? The answer: usually, we do this when we want to run something in the background while continuing to process the request and return a response as soon as possible. In this paradigm, it is beneficial to execute coroutines in the order they were enqueued. For other scenarios, it might be a better approach for a child coroutine to execute immediately. In that case, these scenarios should be considered, and it may be worth introducing specific semantics for such cases. Won't php code behave exactly the same as it did before once enabling the
> scheduler?
Suppose we have a sleep() function. Normally, it calls php_sleep((unsigned int)num). The php_sleep function blocks the execution of the thread. But we need to add an alternative path: if (IN_ASYNC_CONTEXT) { async_wait_timeout((unsigned int) num * 1000, NULL); RETURN_LONG(0); } The IN_ASYNC_CONTEXT condition consists of two points: - The current execution context is inside a *Fiber*. - The *Scheduler* is active. What’s the difference? If the *Scheduler* is not active, calling sleep() will block the entire *Thread* because, without an event loop, it simply cannot correctly handle concurrency. However, if the *Scheduler* is active, the code will set up handlers and return control to the "main loop", which will pick the next Fiber from the queue, and so on. This means that *without a Scheduler and Reactor, concurrent execution is impossible (*without additional effort*)*. From the perspective of a PHP developer, if they are working with *AMPHP/Swoole*, nothing changes, because the code inside the if condition will *never execute* in their case. Does this change the execution order inside a *Fiber*? No. If you had code working with *RabbitMQ sockets*, and you copied this code into a *Fiber*, then enabled concurrency, it would work exactly the same way. If the code used *blocking sockets*, the *Fiber* would yield control to the *Scheduler*. And if two such *Fibers* are running, they will start working with *RabbitMQ sequentially*. Of course, each *Fiber* should use a different socket. The same applies to *CURL*. Do you have an existing module that sends requests to a service using *CURL* in a synchronous style? Just copy the code into a coroutine. This means *almost* 98% transparency. Why *almost*? Because there might be nuances in *helper functions* and *internal states*. There may also be *differences in OS state management* or *file system*, which could affect the final result.
> How will a library take advantage of this feature if it cannot be certain the scheduler is > running or not? Do I need to write a library for async and another version for non-async? > Or do all the async functions with this feature work without the scheduler running, or do > they throw a catchable error? > > This means that the launchScheduler() function should be called *only
once* during the entire lifecycle of the application. If an error occurs and is not handled, the application should *terminate*. This is not a technical limitation but rather a *logical constraint*. If launchScheduler() were replaced with a CLI option, such as php --enable-scheduler, where the *Scheduler* is implicitly activated, then it would be like the *last line of code *it must exist *only once*. Will this change the way os signals are handled then? Will it break compatibility if a
> library uses pcntl traps and I'm using true async traps too? Note there are several > different ways (timeout) signals are handled in PHP -- so if (per-chance) the scheduler > could always be running, maybe we can unify the way signals are handled in php. > > > Regarding this phrase in the RFC: it refers to the *window close event*
in Windows, which provides a few seconds before the process is forcibly terminated. There are signals intended for *application termination*, such as *SIGBREAK* or *CTRL-C*, which should typically be handled in *only one place* in the application. Developers are often tempted to insert signal handlers in multiple locations, making the code dependent on the environment. But more importantly, this *should not happen at all*. *True Async* explicitly defines a *Flow* for emergency or unexpected application termination. Attempting to disrupt this *Flow* by adding a custom termination signal handler introduces *ambiguity*. There should be *only one* termination handler. And at the end of its execution, it *must* call gracefulShutdown. As for *pcntl*, this will need to be tested.
> What if it never resumes at all?
If a *Fiber* is never resumed, it means the application has completely crashed with no way to recover :) The RFC has *two sections* dedicated to this issue: *Cancellation Operation* + *Graceful Shutdown*. If the application *terminates due to an unhandled exception*, *all Fibers must be executed*. Any *Fiber* can be canceled *at any time*, and there is *no need* to use *explicit Cancellation*, which I personally find an inconvenient pattern. The RFC doesn’t mention the stack trace. Will it throw away any information
> about the inner exception?
This is literally *"exception transfer"*. The stack trace will be exactly the same as if the exception were thrown at the call site. To be honest, I haven’t had enough time to thoroughly test this. Let's try it: <?php Async\async(function() { echo "async function 1\n"; Async\async(function() { echo "2\n"; throw new Error("Error"); }); }); echo "start\n"; try { Async\launchScheduler(); } catch (\Throwable $exception) { print_r($exception); } echo "end\n"; ?> 004+ Error Object 005+ ( 006+ [message:protected] => Error 007+ [string:Error:private] => 008+ [code:protected] => 0 009+ [file:protected] => async.php 010+ [line:protected] => 8 011+ [trace:Error:private] => Array 012+ ( 013+ [0] => Array 014+ ( 015+ [function] => {closure:{closure:async.php:3}:6} 016+ [args] => Array 017+ ( 018+ ) 019+ ) 020+ [1] => Array 021+ ( 022+ [file] => async.php 023+ [line] => 14 024+ [function] => Async\launchScheduler 025+ [args] => Array 026+ ( 027+ ) 028+ ) 029+ ) 030+ [previous:Error:private] => 031+ ) Seems perfectly correct. What will calling exit or die do? I completely forgot about them! Well, of course, Swoole override them. This needs to be added to the TODO. Why is this the case? For example, consider a *long-running* application where a *service* is a class that remains in memory continuously. The *web server* receives an HTTP request and starts a *Fiber* for each request. Each request has its own *User Session ID*. You want to call a service function, but you *don’t* want to pass the *Session ID* every time, because there are also *5-10 other request-related variables*. However, you *cannot* simply store the *Session ID* in a class property, because *context switching is unpredictable*. At one moment, you're handling *Request #1*, and a second later, you're already processing *Request #2*. When a *Fiber* creates another *Fiber*, it copies a *reference* to the *context object*, which has *minimal performance impact* while maintaining execution *environment consistency*. *Closure variables work as expected *they are pure *closures* with no modifications. I didn’t mean that *True Async* breaks anything at the language level. The issue is *logical*: You *cannot* use a *global variable* in two *Fibers*, modify it, read it, and expect its state to remain *consistent*. By this point we have covered FiberHandle, Resume, and Contexts. Now we
> have Futures? Can we simplify this to just Futures? Why do we need all > these different ways to handle execution?
*Futures* and *Notifiers* are two different patterns. - A *Future* changes its state *only once*. - A *Notifier* generates *one or more* events. - Internally, *Future* uses *Notifier*. In the *RFC*, I mention that these are essentially *two APIs*: - *High-level API* - *Low-level API* One of the open questions is whether both APIs should remain in *PHP-land*. The *low-level API* allows for close interaction with the *event loop*, which might be useful if someone wants to write a *service* in PHP that requires this level of control. Additionally, this API helps *minimize Fiber context switches*, since its callbacks execute *without switching*. This is *both an advantage and a disadvantage*.
> It's also not clear what the value of most of these function is. For example: > > Your comment made me think, especially in the context of anti-patterns.
And I agree that it's better to remove unnecessary methods than to let programmers shoot themselves in the foot. As for the single producer method, I am not sure why you would use this.
> >
Yes, in other languages there are no explicit restrictions. If the single producer approach is indeed rarely used, then it's not such an important feature to include. However, I lack certainty on whether it's truly a rare case. On the other hand, these functions are inexpensive to implement and do not affect performance. Moreover, they have another drawback: they increase the number of behavioral variants in a single class, which seems a more significant disadvantage than the frequency of use. It isn't clear what happens when `trySend` fails. Is this an error or does nothing?
> > Yes, this is a documentation oversight. I'll add it to the TODO.
Thinking through it, there may be cases where `trySend` is valid, Code using tryReceive could be useful in cases where a channel is used to implement a pool. Suppose you need to retrieve an object from the pool, but if it's not available, you’d prefer to do something else (like throw an exception) rather than block the fiber. Overall, though, you’re right — it’s an antipattern. It’s better to implement the pool as an explicit class and reserve channels for their classic use. Can you expand on what this means in the RFC? Why expose it if it shouldn't
> be used?
I answered a similar question above.
> I also noticed that you seem to be relying heavily on the current > implementation to define
behavior. I love an iterative approach: prototype => RFC => prototype => RFC. Thank you for the excellent remarks and analysis! Ed.

Daniil Gentili

1 year ago
Hi,
> Any Fiber can be canceled at any time, and there is no need to use explicit Cancellation, which I personally find an inconvenient pattern. >
As a heavy use of both amphp and go, cancellations (contexts in go) are absolutely needed, as a fiber may spawn further background fibers in order to execute some operation, just cancelling that specific fiber will not cancel the spawned fibers, unless a bunch of boilerplate try-catch blocks are added to propagate CancellationExceptions. A nicer API should use only explicit cancellation objects, as this pattern of preemptive implicit cancellations (i.e. a fiber may be cancelled at any point via cancel()) is super dangerous IMO, as it can lead to all sorts of nasty behaviour: what if we cancel execution of a fiber in the middle of a critical section (i.e. between a lock() and an unlock() of a file or a database? What if unlocking() in the catch (CancelledException) block requires spawning a new fiber as part of the interaction with the database?). Consider also the huge amount of CancelledException blocks that would have to be added to handle state cleanup in case of premature implicit cancellations, as opposed to explicit cancellations that only throw when we ask them to: there’s a reason why golang, amphp & others use explicit cancellations. Another thing I’m not happy with is how unless the scheduler is launched, all code executes in blocking mode: this seems like a super bad idea, as it will hold back the ecosystem again, and create a split in the project similar to JIT (i.e. a separate “execution mode” with its own bugs, that get fixed slowly because few people are using it, and few people are using it because of its bugs). The main reason given in the RFC (Code written without using the Scheduler should not experience any side effects) makes no sense, because legacy code not spawning fibers will not experience concurrency side effects anyway, regardless of whether the scheduler is started or not. A thing I would love to see, on the other hand, is for Context to become a “provider” for superglobals such as $_REQUEST, $_POST, $_GET, and all globals in general (and perhaps all other global state such as static properties): this would allow to very easily to turn i.e. php-fpm into a fully asynchronous application server, where each request is started in the same thread (or in N threads in an M-N M>N execution model) but its global state is entirely isolated between fibers. Regards, Daniil Gentili - Senior software engineer Portfolio: https://daniil.it <https://daniil.it/> Telegram: https://t.me/danogentili

Edmond Dantes

1 year ago
> > As a heavy use of both amphp and go, cancellations (contexts in go) are > absolutely needed, as a fiber may spawn further background fibers in order > to execute some operation, just cancelling that specific fiber will not > cancel the spawned fibers, unless a bunch of boilerplate try-catch blocks > are added to propagate CancellationExceptions.
I didn't mean that Cancellation isn't needed at all. I meant that canceling a Fiber is sufficient in most scenarios and leads to clean, understandable code. Other languages have child coroutines (Swoole supports them too), but I'm not sure if that's the right approach. I like context.WithCancel from Go, but it can essentially be implemented directly in PHP land since all the necessary tools are available. A nicer API should use only explicit cancellation objects, as this pattern
> of preemptive implicit cancellations
The exception mechanism is the standard way to alter the execution flow in PHP. If a programmer writes code with lock and unlock outside of a try-finally block but calls functions between these methods, they are potentially creating a bad solution—at the very least because someone else might later introduce an exception in one of those functions. This is a classic case for languages with exceptions. So far, I haven't found a better way to ensure the logical consistency and integrity of the execution flow. Maybe someone has a suggestion?
> The main reason given in the RFC > > The main reason is that PHP has been around for many years and didn’t just
appear yesterday. If you have an idea on how to start the Scheduler implicitly, let's implement it. So far, I have a few ideas: 1. Using an option in php.ini (downside: if PHP is used for multiple projects). 2. Using a CLI option – so far, I like this the most. A thing I would love to see, on the other hand, is for Context to become a “provider” It's hard for me to evaluate this idea. Intuitively, it doesn't seem ideal. In general, I'm not very fond of $_GET/$_POST. But on the other hand, why not? This needs some consideration. allow to very easily to turn i.e. php-fpm into a fully asynchronous application server,
> where each request is started in the same thread (or in N threads in an M-N M>N > execution model) but its global state is entirely isolated between fibers. > > I haven’t thought about this possibility. But wouldn’t this break the FCGI
contract? Thanks! Ed.

Daniil Gentili

1 year ago
> On 3 Mar 2025, at 13:05, Edmond Dantes <edmond.ht@gmail.com> wrote: > >> As a heavy use of both amphp and go, cancellations (contexts in go) are absolutely needed, as a fiber may spawn further background fibers in order to execute some operation, just cancelling that specific fiber will not cancel the spawned fibers, unless a bunch of boilerplate try-catch blocks are added to propagate CancellationExceptions. > > I didn't mean that Cancellation isn't needed at all. I meant that canceling a Fiber is sufficient in most scenarios and leads to clean, understandable code. > > Other languages have child coroutines (Swoole supports them too), but I'm not sure if that's the right approach. > > I like context.WithCancel from Go, but it can essentially be implemented directly in PHP land since all the necessary tools are available. >
Note, this is precisely the problem, implement cancellation propagation to child fibers in userland PHP requires writing a bunch of boilerplate try-catch blocks to propagate CancellationExceptions to child FutureHandle::cancel()s (spawning multiple fibers to execute subtasks concurrently during an async method call is pretty common, and the current implicit cancellation mode requires writing a bunch of try-catch blocks to propagate cancellation, instead of just passing a cancellation object, or a flag to inherit the cancellation of the current fiber when spawning a new one).
>> A nicer API should use only explicit cancellation objects, as this pattern of preemptive implicit cancellations > > The exception mechanism is the standard way to alter the execution flow in PHP. If a programmer writes code with lock and unlock outside of a try-finally block but calls functions between these methods, they are potentially creating a bad solution—at the very least because someone else might later introduce an exception in one of those functions. This is a classic case for languages with exceptions. >
Note the explicit use case I listed is that of an unlock() in a finally block that *requires spawning a new fiber* in order to execute the actual unlock() RPC call: this is explicitly in contrast with the RFC, which specifies that
>ATTENTION: A programmer must never attempt to create a new fiber while handling a CancellationException, as this behavior may trigger an exception during Graceful Shutdown mode.
While this is *somewhat* understandable in the context of graceful shutdown, it still means that unlocking in a finally block (the only way of properly handling cancellations with the current model) isn’t always possible..
> So far, I haven't found a better way to ensure the logical consistency and integrity of the execution flow. Maybe someone has a suggestion? > >> The main reason given in the RFC > > The main reason is that PHP has been around for many years and didn’t just appear yesterday. > > If you have an idea on how to start the Scheduler implicitly, let's implement it. So far, I have a few ideas: > > Using an option in php.ini (downside: if PHP is used for multiple projects). > Using a CLI option – so far, I like this the most.
I would really prefer it to be always enabled, no fallback at all, because as I said, it will make absolutely no difference to legacy, non-async projects that do not use fibers, but it will avoid a split ecosystem scenario.
>> A thing I would love to see, on the other hand, is for Context to become a >> “provider” > > It's hard for me to evaluate this idea. Intuitively, it doesn't seem ideal. In general, I'm not very fond of $_GET/$_POST. But on the other hand, why not? This needs some consideration. > >> allow to very easily to turn i.e. php-fpm into a fully asynchronous application server, >> where each request is started in the same thread (or in N threads in an M-N M>N >> execution model) but its global state is entirely isolated between fibers. > I haven’t thought about this possibility. But wouldn’t this break the FCGI contract?
I see no reason why it should break the contract, if implemented by isolating the global state of each fiber, it can be treated as a mere implementation detail of the (eventually new) SAPI. Regards, Daniil Gentili — Daniil Gentili - Senior software engineer Portfolio: https://daniil.it <https://daniil.it/>Telegram: https://t.me/danogentili

Edmond Dantes

1 year ago
I like context.WithCancel from Go, but it can essentially be implemented directly in PHP land since all the necessary tools are available. Note, this is precisely the problem, implement cancellation propagation to child fibers in userland PHP requires writing a bunch of boilerplate try-catch blocks to propagate CancellationExceptions to child FutureHandle::cancel()s (spawning multiple fibers to execute subtasks concurrently during an async method call is pretty common, and the current implicit cancellation mode requires writing a bunch of try-catch blocks to propagate cancellation, instead of just passing a cancellation object, or a flag to inherit the cancellation of the current fiber when spawning a new one). Catching CancellationException is only necessary if there is some defer code. If there isn't, then there's no need to catch it. Try-catch blocks are not mandatory. We can create a Cancellation object, pass it via use or as a parameter to all child fibers, and check it in await(). This is the most explicit approach. In this case, try-catch would only be needed if we want to clean up some resources. Otherwise, we can omit it. According to the RFC, if a fiber does not catch CancellationException, it will be handled by the Scheduler. Therefore, catching this exception is not strictly necessary. If this solution also seems too verbose, there is another one that can be implemented without modifying this RFC. For example, implementing a cancellation operation for a Context. All coroutines associated with this context would be canceled. From an implementation perspective, this is essentially iterating over all coroutines and checking which context they belong to. Note the explicit use case I listed is that of an unlock() in a finally block that *requires spawning a new fiber* in order to execute the actual unlock() RPC call: this is explicitly in contrast with the RFC, which specifies that So, if I understand correctly, the code in question looks like this: try { lock(); ... } finally { unlock(); } function unlock() { async\run(); } If I got it right, then the following happens: The code inside try {} allocates resources. The code inside finally {} also allocates resources. So, what do we get? We're trying to terminate the execution of a fiber, and instead, it creates a new one. It seems like there's a logical error here. Instead of creating a new fiber, it would be better to use microtasks. I would really prefer it to be always enabled, no fallback at all, because as I said, it will make absolutely no difference to legacy, non-async projects that do not use fibers, but it will avoid a split ecosystem scenario. I'm not arguing at all that avoiding the call to this function is a good solution. I’m on your side. The only question is how to achieve this technically. Could you describe an example of "ecosystem split" in the context of this function? What exactly is the danger? I see no reason why it should break the contract, if implemented by isolating the global state of each fiber, it can be treated as a mere implementation detail of the (eventually new) SAPI. So, I can take NGINX and FCGI, and without changing the FCGI interface itself, but modifying its internal implementation, get a working application. Yes, but... that means all global variables, including static ones, need to be tied to the context. It's not that it can't be done, but what about memory consumption. I'm afraid that if the code wasn't designed for a LongRunning APP, it's unlikely to handle this task correctly.
-- Ed.

Edmond Dantes

1 year ago
Lock/Unlock issue It seems that this is actually about a database query that puts the Fiber into a waiting state specifically, query("UNLOCK"). In that case, everything should work correctly. Although there are some dangerous edge cases. The database might be under high load, causing the query("UNLOCK") request to wait for too long, leading to a timeout. This would trigger another exception, which could then be interpreted as a complete failure. Putting a Fiber into a waiting state inside a finally block does not contradict the shutdown mode. However, the programmer must be careful inside finally section because if a second exception occurs, it means the code cannot properly complete execution.
-- Ed.

Nicolas Grekas

1 year ago
Hi Edmond, Thanks for sharing the huge amount of work that went into this! I would really prefer it to be always enabled, no fallback at all, because
> as I said, it will make absolutely no difference to legacy, non-async > projects that do not use fibers, but it will avoid a split ecosystem > scenario. > > > I'm not arguing at all that avoiding the call to this function is a good > solution. I’m on your side. The only question is *how* to achieve this > technically. > > Could you describe an example of *"ecosystem split"* in the context of > this function? What exactly is the danger? >
Not sure it's an answer to this question but in Symfony's HttpClient, we have an amphp-based implementation that's working both outside and inside an event loop: - inside means amphp's scheduler already started, and then each request is scheduled thanks to amphp's http client - outside means Symfony's code is going to trigger amphp's event loop internally. The target DX is that when outside any event loop, we're still able to leverage fibers to provide concurrency, for requests only, and when inside an event loop, requests run concurrently to any other things that the loop monitors. Is that something that could be achieved with your proposal? If not, maybe that's the split we're wondering about? Nicolas

Edmond Dantes

1 year ago
Hi, Nicolas. Hi Edmond, The target DX is that when outside any event loop, we're still able to leverage fibers to provide concurrency, for requests only, and when inside an event loop, requests run concurrently to any other things that the loop monitors. Is that something that could be achieved with your proposal? If not, maybe that's the split we're wondering about? This RFC leads to PHP operating in two modes: Blocking mode: The Event Loop needs to be implemented manually, AMPHP works. This is how PHP currently operates. Concurrent mode: Code runs in coroutines. The Event Loop works under the hood. AMPHP does not work. If we try to imagine a way to keep PHP in a single mode, it would likely require implementing coroutines separately from Fiber and leaving Fiber as legacy. This solution has both advantages and disadvantages. Advantages: Switching can be optimized considering the new architecture. The Event Loop will start automatically when needed. Code using Fiber will work as before, and most likely, AMPHP will be able to create an event loop in user-land. Disadvantages: More work is required. There is a risk of ending up with a Frankenstein-like result. :) A relative advantage of the current implementation is that it changes only about 100-500 lines in the PHP core (probably even fewer, since part of the changes are in extensions like CURL and Socket). The downside is that it cannot change the rules that were previously established.
-- Ed.

Edmond Dantes

1 year ago
> > Note the explicit use case I listed is that of an unlock() in a finally > block that *requires spawning a new fiber* in order to execute the actual > unlock() RPC call: this is explicitly in contrast with the RFC, which > specifies that > >*ATTENTION*: A programmer must *never* attempt to create a new fiber > while handling a CancellationException, as this behavior may trigger an > exception during *Graceful Shutdown* mode. > >
I think you are right. This restriction increases complexity without providing significant benefits. I will remove this condition from the RFC entirely and simply state that the programmer should handle such situations carefully. Thank you!

Larry Garfield

1 year ago
On Sat, Mar 1, 2025, at 3:11 AM, Edmond Dantes wrote:
> Good day, everyone. I hope you're doing well. > > I’d like to introduce a draft version of the RFC for the True Async component. > > https://wiki.php.net/rfc/true_async > > I believe this version is not perfect and requires analysis. And I > strongly believe that things like this shouldn't be developed in > isolation. So, if you think any important (or even minor) aspects have > been overlooked, please bring them to attention. > > The draft status also highlights the fact that it includes doubts about > the implementation and criticism. The main global issue I see is the > lack of "future experience" regarding how this API will be used—another > reason to bring it up for public discussion. > > Wishing you all a great day, and thank you for your feedback!
I finally managed to read through enough of the RFC to say something intelligent. :-) First off, as others have said, thank you for a thorough and detailed proposal. It's clear you've thought through a lot of details. I also especially like that it's transparent for most IO operations, which is mandatory for adoption. It's clear to me that async in PHP will never be more than niche until there is a built-in dev-facing API that is easy to use on its own without any 3rd party libraries. Unfortunately, at this point I cannot support this proposal, because I disagree with the fundamental design primitives. Let's look at the core design primitives: * A series of free-standing functions. * That only work if the scheduler is active. * The scheduler being active is a run-once global flag. * So code that uses those functions is only useful based on a global state not present in that function. * And a host of other seemingly low-level objects that have a myriad of methods on them that do, um, stuff. * Oh, and a lot of static methods, too, instead of free-standing functions. The number of ways for this to go wrong and confuse the heck out of a developer is disturbingly high. In the Low-Level API section, the RFC notes:
> I came to the conclusion that, in the long run, sacrificing flexibility in favor of code safety is a reasonable trade-off.
I completely agree with this statement! And feel the RFC doesn't go even remotely far enough in that direction. In particular, I commend to your attention this post about a Python async library that very deliberately works at a much higher level of abstraction, and is therefore vastly safer: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ I won't repeat the post, but suffice to say I agree with it almost entirely. (I dislike the name "nursery," but otherwise...) That is the direction we should be looking at for PHP, from the get-go. PHP doesn't have Python-style context managers (though I would like them), so a PHP version of that might look something like this (just spitballing): async $context { // $context is an object of AsyncContext, and can be passed around as such. // It is the *only* way to span anything async, or interact with the async controls. // If a function doesn't take an AsyncContext param, it cannot control async. This is good. $context->run(some_function(...)); $result = $context->run(function(AsyncContext $ctx) use ($someObj) { // This queues a thunk to run at the end of the closest async {} block. $ctx->defer($someObj->shutdown(...)); }); } catch (SomeException $e) { // Exception thrown by one of the fibers. } // This is an unwrapped value. print $result; Naturally there would be more to the API, but I'm just showing the basics. Importantly: * There is no global modal (schedulerStarted) to think about. * When the async {} block ends, you know with 100% certainty that there are no dangling background tasks. * It's explicitly obvious what functions are going to try and mess with the async context, and therefore cannot be called except within an async context. * An application can have sync portions and async portions very easily, without worrying about which "mode" it's in at a given time. It also means that writing a number of the utilities mentioned in the RFC do not require any engine code. Eg: function parallel_map(iterable $it, Closure $fn) { $result = []; async $ctx { foreach ($it as $k => $v) { $result[$k] = $ctx->run($fn($v)); } } return $result; } Now I know that's safe to call anywhere, whether I'm current in an active async mode or not. I'm not convinced that sticking arbitrary key/value pairs into the Context object is wise; that's global state by another name. But if we must, the above would handle all the inheritance and override stuff quite naturally. Possibly with: async $ctx from $parentCtx { // ... } Similarly, the two different modes for channels strike me as quite unnecessary. I also would tend to favor how Rust does channels (via a library, I don't think it's a built-in): have separate variables for the in-side and out-side. Again, just spitballing: [$in, $out] = Channel::create($buffer_size); $in->send($val); $out->receive($val); (Give or take variations of those methods.) Now you don't need to worry about fibers owning things. You just have a ChannelIn object and a ChannelOut object, and can pass either one to as many or as few functions as you want. And those functions could be spawning new fibers if you'd like, or not. (There's likely some complications here I'm not thinking of, but I've not dug into it in depth yet.) You can now close either side, or just let the objects go out of scope. In short, I am fully in favor of better async logic in PHP. I am very against an API that even allows me to do something stupid or deadlock-creating, or that relies on hidden global state. That would be worse than the status quo, and there are better models than what is shown here that offer much stronger "correct by construction" guarantees. --Larry Garfield

[ ]

1 year ago
Hi there, I would also like to highlight some interesting ideas that I find being useful to consider. Recently Bend programming language has been released, and it incorporates a completely different view on the conception of "code", in the definition of "what it is" and "how it should be interpreted". While we interpret it as a sequence of instructions, the proper way of seeing it is the graph of instructions. On every step we reduce that graph, by running the code of the nodes current node depends on. Therefore, basically everything could paralleled w/o the need to have fancy management of threads and other low-level things. For example, having this code: $foo = foo(); $bar = bar(); $baz = $foo + $bar; If it was run in Bend, it would be interpreted so that foo() and bar() functions are executed in parallel, and $baz = $foo + $bar is executed afterwards, since this computation depends on the other two. The key, most excellently beautiful feature here is that all async management is under the hood, exposing nothing for the developers to be bothered with. That being said, I also want to mention that Bend has a primitive for concurrent loops. Actually, they used another solution, different from loops, since loops are sequential by their essense (iterative one by one). They introduced a concurrent alternative for loops with "bend" keyword, allowing data structures to be traversed in parallel. I think this is actually "the right way" of doing parallel processing in general and async programming in particular, and this is greatly to be considered for having at least some principles applied in PHP. What I think it could be. async function baz(): int { $foo = foo(); $bar = bar(); return $foo + $bar; } // value is returned just like from any other ordinary function $val = baz(); Function above could run foo() in one fiber, and bar() in another, both of them being awaited at the return statement (at the first statement where the value is actually used / referenced, if we put it more generally) so that actual values could be taken. In other words, async function is not promise-based as in other languages that suffer from red blue function problem, but rather it is function with coroutine flow of execution, so that foo() is executed as the first coroutine, and when it blocks, then bar() is executed until it also blocks. Then, at plus operator being evaluated, $foo is awaited, and $bar is awaited, since they are necessary parts for + operation to complete. Best regards

Rob Landers

1 year ago
On Tue, Mar 4, 2025, at 23:54, Eugene Sidelnyk wrote:
> Hi there, > > I would also like to highlight some interesting ideas that I find being useful to consider. > > Recently Bend programming language has been released, and it incorporates a completely different view on the conception of "code", in the definition of "what it is" and "how it should be interpreted". > > While we interpret it as a sequence of instructions, the proper way of seeing it is the graph of instructions. On every step we reduce that graph, by running the code of the nodes current node depends on. > > Therefore, basically everything could paralleled w/o the need to have fancy management of threads and other low-level things. > > For example, having this code: > > $foo = foo(); > $bar = bar(); > $baz = $foo + $bar; > > If it was run in Bend, it would be interpreted so that foo() and bar() functions are executed in parallel, and $baz = $foo + $bar is executed afterwards, since this computation depends on the other two. > > The key, most excellently beautiful feature > here is that all async management is under the hood, exposing nothing for the developers to be bothered with. > > That being said, I also want to mention that Bend has a primitive for concurrent loops. Actually, they used another solution, different from loops, since loops are sequential by their essense (iterative one by one). They introduced a concurrent alternative for loops with "bend" keyword, allowing data structures to be traversed in parallel. > > I think this is actually "the right way" of doing parallel processing in general and async programming in particular, and this is greatly to be considered for having at least some principles applied in PHP. > > What I think it could be. > > async function baz(): int { > $foo = foo(); > $bar = bar(); > > return $foo + $bar; > } > > // value is returned just like from any other ordinary function > $val = baz(); > > Function above could run foo() in one fiber, and bar() in another, both of them being awaited at the return statement (at the first statement where the value is actually used / referenced, if we put it more generally) so that actual values could be taken. > > In other words, async function is not promise-based as in other languages that suffer from red blue function problem, but rather it is function with coroutine flow of execution, so that foo() is executed as the first coroutine, and when it blocks, then bar() is executed until it also blocks. Then, at plus operator being evaluated, $foo is awaited, and $bar is awaited, since they are necessary parts for + operation to complete. > > > Best regards
Huh. Reminds me of SSA, which can identify independent computations like that. It’s used by go, and many other compiled languages, but not in the same way this bend language does it. So, that’s interesting. I don’t know if php could implement SSA (maybe opcache could), but with how dynamic php is, I’m not sure it would be helpful. An interesting application nonetheless, thanks for sharing! — Rob

Edmond Dantes

1 year ago
Hello, Eugene! What I think it could be. async function baz(): int { $foo = foo(); $bar = bar(); return $foo + $bar; } // value is returned just like from any other ordinary function $val = baz(); If we have code like $x + $y, and in one block it follows rule 1 while in another block it follows rule 2, this increases the complexity of the language. The worst part is that the same operators exhibit DIFFERENT behavior in different contexts. This violates semantic integrity. (A similar issue occurred in C++ with operator overloading, where a theoretically elegant solution turned out to be terrible in practice). If you want to achieve a clean syntax for concurrency in PHP, I would suggest considering pipes in the long run. For example: |> $users = getUsers() ||| $orders = getOrders() |> mergeByColumn($users, $orders, 'orders')
-- Ed.

Weedpacket

1 year ago
On 2025-03-05 11:54, Eugene Sidelnyk wrote:
> Hi there, > > I would also like to highlight some interesting ideas that I find
being useful to consider.
> > Recently Bend programming language has been released, and it
incorporates a completely different view on the conception of "code", in the definition of "what it is" and "how it should be interpreted".
> > While we interpret it as a sequence of instructions, the proper way
of seeing it is the graph of instructions. On every step we reduce that graph, by running the code of the nodes current node depends on.
>
I've always kind of liked this model. https://en.wikipedia.org/wiki/Dataflow_programming

Rowan Tommins [IMSoP]

1 year ago
On 4 March 2025 18:36:37 GMT, Larry Garfield <larry@garfieldtech.com> wrote:
>PHP doesn't have Python-style context managers (though I would like them)
So would I, I've actually thought about it a lot... But more importantly, this highlights something important about that Python library: it is built *on top of* a native async/await system which is baked into the language (note the example uses "async with", not normal "with"). That reinforces my earlier feeling that this RFC is trying to do far too much at once - it's not just about "low-level vs high-level", there's multiple whole features here: - asynchronous versions of native functions, and presumably a C API for writing those in extensions - facilities for writing coroutines (async/await, but not as keywords) - deferrable "microtasks" - event/signal handling functionality - communication between threads/fibers via Channels - a facility for launching coroutines concurrently (as Python demonstrates, this can be separate from how the coroutines themselves are written) - maybe more that I've overlooked while trying to digest the RFC Having all of those would be amazing, but every one of them deserves its own discussion, and several can be left to userland or as future scope in an initial implementation. Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
Good day, Larry.
> First off, as others have said, thank you for a thorough and detailed
proposal. Thanks!
> * A series of free-standing functions. > * That only work if the scheduler is active. > * The scheduler being active is a run-once global flag. > * So code that uses those functions is only useful based on a global
state not present in that function.
> * And a host of other seemingly low-level objects that have a myriad of
methods on them that do, um, stuff.
> * Oh, and a lot of static methods, too, instead of free-standing
functions. Suppose these shortcomings don’t exist, and we have implemented the boldest scenario imaginable. We introduce Structured Concurrency, remove low-level elements, and possibly even get rid of Future. Of course, there are no functions like startScheduler or anything like that. 1. In this case, how should PHP handle Fiber and all the behavior associated with it? Should Fiber be declared deprecated and removed from the language? What should the flow be? 2. What should be done with I/O functions? Should they remain blocking, with a separate API provided as an extension? 3. Would it be possible to convince the maintainers of XDEBUG and other extensions to rewrite their code to support the new model? ( *If you're reading this question now, please share your opinion.* ) 4. If transparent concurrency is introduced for I/O in point 2, what should be done with Revolt + AMPHP? This would break their code. Should an additional function or option be introduced to switch PHP into "legacy mode"? I share your feelings on many points, but I would like to see some real-world alternative.
> > I commend to your attention this post about a Python async library >
Structured concurrency is a great thing. However, I’d like to avoid changing the language syntax and make something closer to Go’s semantics. I’ll think about it and add this idea to my TODO.
> async $context { > // $context is an object of AsyncContext, and can be passed around as
such.
> // It is the *only* way to span anything async, or interact with the
async controls.
> // If a function doesn't take an AsyncContext param, it cannot control
async. This is good. This is a very elegant solution. Theoretically. However, in practice, if you require explicitly passing the context to all functions, it leads to the following consequences: 1. The semantics of all functions increase by one additional parameter (*Signature bloat*). 2. If an asynchronous call needs to be added to a function, and other functions depend on it, then the semantics of all dependent functions must be changed as well. In strict languages, a hybrid model is often used, or like in Go, where the context is passed explicitly as a synchronization object, but only when necessary. In this example, there is another aspect: the fact that async execution is explicitly limited to a specific scope. This is essentially the same as startScheduler, and it is one of the options I was considering. Of course, startScheduler can be replaced with a construction like async(function() { ... }). This means that async execution is only active within the closure, and coroutines can only be created inside that closure. This is one of the semantic solutions that allows removing startScheduler, but at the implementation level, it is exactly the same. What do you think about this?
> I'm not convinced that sticking arbitrary key/value pairs into the
Context object is wise; Why not?
> that's global state by another name
Static variables inside a function are also global state. Are you against static variables?
> But if we must, the above would handle all the inheritance and override
stuff quite naturally. Possibly with: How will a context with open string keys help preserve service data that the service doesn't want to expose to anyone? The Key() solution is essentially the same as Symbol in JS, which is used for the same purpose. Of course, we could add a coroutine static $var construct to the language syntax. But it's all the same just syntactic sugar that would require more code to support.
> [$in, $out] = Channel::create($buffer_size);
This semantics require the programmer to remember that two variables actually point to the same object. If a function has multiple channels, this makes the code quite verbose. Additionally, such channels are inconvenient to store in lists because their structure becomes more complex. I would suggest a slightly different solution: <code php> $in = new Channel()->getProducer(); async myFunction($in->getConsumer()); <code> This semantics do not restrict the programmer in usage patterns while still allowing interaction with the channel through a well-defined contract. Thanks for the great examples, and a special thanks for the article. I also like the definition of context. Ed

Larry Garfield

1 year ago
On Wed, Mar 5, 2025, at 3:37 AM, Edmond Dantes wrote:
> Good day, Larry. > >> First off, as others have said, thank you for a thorough and detailed proposal. > Thanks! > >> * A series of free-standing functions. >> * That only work if the scheduler is active. >> * The scheduler being active is a run-once global flag. >> * So code that uses those functions is only useful based on a global state not present in that function. >> * And a host of other seemingly low-level objects that have a myriad of methods on them that do, um, stuff. >> * Oh, and a lot of static methods, too, instead of free-standing functions. > > Suppose these shortcomings don’t exist, and we have implemented the > boldest scenario imaginable. We introduce Structured Concurrency, > remove low-level elements, and possibly even get rid of `Future`. Of > course, there are no functions like `startScheduler` or anything like > that. > > 1. In this case, how should PHP handle `Fiber` and all the behavior > associated with it? Should `Fiber` be declared deprecated and removed > from the language? What should the flow be?
I'm not sure yet. I was quite hesitant about Fibers when they went in because they were so low-level, but the authors were confident that it was enough for a user-space toolchain to be iterated on quickly that everyone could use. That clearly didn't pan out as intended (Revolt exists, but usage of it is still rare), so here we are with a half-finished API. Thinking aloud, perhaps we could cause `new Fiber` to create an automatic async block? Or we do deprecate it and discourage its use. Something to think through, certainly.
> 2. What should be done with I/O functions? Should they remain > blocking, with a separate API provided as an extension?
The fact that IO functions become transparently async when appropriate is the best part of the current RFC. Please keep that. :-)
> 3. Would it be possible to convince the maintainers of XDEBUG and > other extensions to rewrite their code to support the new model? ( *If > you're reading this question now, please share your opinion.* )
I cannot speak for Derick.
> 4. If transparent concurrency is introduced for I/O in point 2, what > should be done with `Revolt` + `AMPHP`? This would break their code. > Should an additional function or option be introduced to switch PHP > into "legacy mode"?
Also an excellent question, to which I do not yet have an answer. (See previous point about Fibers being half-complete.) I would want to involve Aaron, Christian, and Ces-Jan before trying to make any suggestions here.
> Structured concurrency is a great thing. However, I’d like to avoid > changing the language syntax and make something closer to Go’s > semantics. I’ll think about it and add this idea to my TODO.
Well, as noted in the article, structured concurrency done right means *not* having unstructured concurrency. Having Go-style async and then building a structured nursery system on top of it means you cannot have any of the guarantees of the structured approach, because the other one is still poking out the side and leaking. We're already stuck with mutable-by-default, global variables, and other things that prevent us from making helpful assumptions. Please, let's try to avoid that for async. We don't need more gotos.
>> async $context { >> // $context is an object of AsyncContext, and can be passed around as such. >> // It is the *only* way to span anything async, or interact with the async controls. >> // If a function doesn't take an AsyncContext param, it cannot control async. This is good. > > This is a very elegant solution. Theoretically. > > However, in practice, if you require explicitly passing the context to > all functions, it leads to the following consequences: > > 1. The semantics of all functions increase by one additional parameter > (*Signature bloat*).
No, just those functions/objects that necessarily involve running async control commands. Most wouldn't. They would just silently context switch when they hit an IO operation (which as noted above is transparency supported, which is what makes this work) and otherwise behave the same. But if something does actively need to do async stuff, it should have a context to work within. It's the same discussion as: A: "Pass/inject a DB connection to a class that needs it, don't just call a global db() function." B: "But then I have to pass it to all these places explicitly!" A: "That's a sign your SQL is too scattered around the code base. Fix that first and your problem goes away." Explicit flow control is how you avoid bugs. It's also self-documenting, as it's patently obvious what code expects to run in an async context and which doesn't care.
> 2. If an asynchronous call needs to be added to a function, and other > functions depend on it, then the semantics of all dependent functions > must be changed as well.
This is no different than DI of any other service. I have restructured code to handle temporary contexts before. (My AttributeUtils and Serde libraries.) The result was... much better code than I had before. I'm glad I made those refactors.
> In this example, there is another aspect: the fact that async execution > is explicitly limited to a specific scope. This is essentially the same > as `startScheduler`, and it is one of the options I was considering. > > Of course, `startScheduler` can be replaced with a construction like > `async(function() { ... })`. > This means that async execution is only active within the closure, and > coroutines can only be created inside that closure. > > This is one of the semantic solutions that allows removing > `startScheduler`, but at the implementation level, it is exactly the > same. > > What do you think about this?
That looks mostly like the async block syntax I proposed, spelled differently. The main difference is that the body of the wrapped function would need to explicitly `use` any variables from scope that it wanted, rather than getting them implicitly. Whether that's good or bad is probably subjective. But it would allow for a syntax like this for the context, which is quite similar to how database transactions are often done: $val = async(function(AsyncContext $ctx) use ($stuff, $fn) { $result = []; foreach ($stuff as $item) { $result[] = $ctx->run($fn); } // We block/wait here until all subtasks are complete, then the async() call returns this value. return $result; }); And of course in both cases you could use a pre-defined callable instead of inlining one. At this point I think it's mostly a stylistic difference, function vs block.
>> I'm not convinced that sticking arbitrary key/value pairs into the Context object is wise; > > Why not? > >> that's global state by another name > > Static variables inside a function are also global state. Are you > against static variables?
Vocally, in fact. :-)
>> But if we must, the above would handle all the inheritance and override stuff quite naturally. Possibly with: > > How will a context with open string keys help preserve service data > that the service doesn't want to expose to anyone? The `Key()` solution > is essentially the same as `Symbol` in JS, which is used for the same > purpose. Of course, we could add a `coroutine static $var` construct to > the language syntax. But it's all the same just syntactic sugar that > would require more code to support.
I cannot speak to JS Symbols as I haven't used them. I am just vhemently opposed to globals, no matter how many layers they're wrapped in. :-) Most uses could be replaced by proper DI or partial application.
>> [$in, $out] = Channel::create($buffer_size); > > This semantics require the programmer to remember that two variables > actually point to the same object. If a function has multiple channels, > this makes the code quite verbose. Additionally, such channels are > inconvenient to store in lists because their structure becomes more > complex. > > I would suggest a slightly different solution: > > <code php> > $in = new Channel()->getProducer(); > async myFunction($in->getConsumer()); > <code> > > This semantics do not restrict the programmer in usage patterns while > still allowing interaction with the channel through a well-defined > contract.
I'd go slightly differently if you wanted to go that route: $ch = new Channel($buffer_size); $in = $ch->producer(); $out = $ch->consumer(); // You do most interaction with $in and $out. I could probably work with that as well. (Or even just $ch->inPipe and $ch->outPipe, now that we have nice property support.) But the overall point, I think, is avoiding implicit modal logic. If my code doesn't need to care if it's in an async world, it doesn't care. If it does, then I need an explicit async world to work within, rather than relying on one implicitly existing, I hope. And I shouldn't have to think about "who owns this end of this channel". I just have an in and out hose I stick stuff into and pull out from, kthxbye.
> Thanks for the great examples, and a special thanks for the article. > I also like the definition of context. > > Ed
--Larry Garfield

Edmond Dantes

1 year ago
> > Thinking aloud, perhaps we could cause `new Fiber` to create an
automatic async block?
>
The main issue with Fibers is their switching logic: If you create Fiber A and call another Fiber B inside it, Fiber B can only return to the Fiber that created it, not just anywhere. However, the Scheduler requires entirely different behavior. This creates a conflict with the Scheduler. Moreover, it can even break the Scheduler if it operates based on Fibers. That's why all these strange solutions in the RFC are just workarounds to somehow bypass this problem. But it seems we've already found an alternative solution.
> I cannot speak for Derick.
Of course, I just mean that he probably won't be happy about it :)
> No, just those functions/objects that necessarily involve running async
control commands. Most wouldn't.
> They would just silently context switch when they hit an IO operation
(which as noted above is transparency supported, which is what makes this
> work) and otherwise behave the same.
So it's something more like Go or Python.
> > $val = async(function(AsyncContext $ctx) use ($stuff, $fn) { > $result = []; > foreach ($stuff as $item) { > $result[] = $ctx->run($fn); >} > > // We block/wait here until all subtasks are complete, then the async()
call returns this value.
> return $result; > });
Do I understand correctly that at the point $val = async(function(AsyncContext $ctx) use ($stuff, $fn) execution stops until everything inside is completed? If so, let me introduce a second semantic option (for now, I'll remove the context and focus only on the function). ```php $url1 = 'https://domain1.com/'; $url2 = 'https://domain2.com/'; $url_handle = fn(string $url) => file_get_contents($url); $res = Async\start(function() use ($url1, $url2, $url_handle) { $res1 = Async\run($url_handle, $url1); $res2 = Async\run($url_handle, $url2); Async\run(fn() => sleep(5)); // some logic here return $merged_result; }); ``` What's Happening Here: 1. After calling $res = Async\start(), the code waits until the entire block completes. 2. Inside Async\start, the code waits for all nested coroutines to finish. 3. If a coroutine has other nested coroutines, the same rule applies. Rules Inside an Asynchronous Block: 1. I/O functions do not block coroutines within the block. 2. Creating a new Fiber is not allowed — an exception will be thrown: you cannot use Fiber. 3. Unhandled exceptions will be thrown at the point of $res = Async\start(). Coroutine Cancellation Rules: Canceling a coroutine cancels it and all its child coroutines (this cannot be bypassed unless the coroutine is created in a different context). How does this option sound to you? Essentially, this is Kotlin, but it should also resemble Python. However, unlike Kotlin, there are no special language constructs here—code blocks naturally serve that role. Of course, syntactic sugar can be added later for better readability. And if you like this, I have good news: there are no implementation issues at this level. In terms of semantic elegance, the only thing that bothers me is that return behavior is slightly altered — meaning the actual "return" won’t happen until all child functions complete. This isn’t very good, and Kotlin’s style would fit better here. But on the other hand — can we live with this?
> I cannot speak to JS Symbols as I haven't used them. > I am just vhemently opposed to globals, no matter how many layers they're
wrapped in. :-) Most uses could be replaced by proper DI or partial application. You won’t be able to use DI because you have only *one service (instance of class)* for the entire application, not a separate service for each coroutine. This service is shared across the application and can be called from any coroutine. As a result, the service needs memory slots to store or retrieve data. DI is a mechanism used once during service initialization, not every time a method is called. The only question is whether to use open text keys in the context, which is unsafe and can lead to collisions, or to use a unique key-object that is known only to the one who created it. (If PHP introduces object constants, this syntax would also look elegant.) There is, of course, another approach: making Context any arbitrary object defined by the user. But this solution has a known downside — lack of a standard interface.
> (Or even just $ch->inPipe and $ch->outPipe, now that we have nice
property support.) Just a brilliant idea. :) Have a good day! Ed.

Rowan Tommins [IMSoP]

1 year ago
On 05/03/2025 21:10, Edmond Dantes wrote:
> Essentially, this is Kotlin, but it should also resemble Python. > However, unlike Kotlin, there are no special language constructs > here—code blocks naturally serve that role. Of course, syntactic sugar > can be added later for better readability.
To pick up on this point: PHP doesn't have any generalised notion of "code blocks", only Closures, and those have a "weight" which is more fundamental than syntax: creating the Closure object, copying or referencing captured variables, creating a new execution stack frame, and arranging for parameters to be passed in and a return value passed out. Perhaps more importantly, there's a reason most languages don't represent flow control purely in terms of functions and objects: it's generally far simpler to define "this is the semantics of a while loop" and implement it in the compiler or VM, than "these building blocks are sufficient that any kind of loop can be built in userland without explicit compiler support". Defining new syntax would encourage us to define a minimum top-level behaviour, such as "inside an async{} block, these things are possible, and these things are guaranteed to be true". Then we simply make that true by having the compiler inject whatever actions it needs before, during, and after that block. Any additional keywords, functions, or objects, are then ways for the user to vary or make use of that flow, rather than ways to define the flow itself. This is roughly what happened with Closures themselves in PHP: first, decide that "$foo = function(){};" will be valid syntax, and define Closure as the type of $foo; then over time, add additional behaviour to the Closure class, the ability to add __invoke() hooks on other classes, etc Regards,
-- Rowan Tommins [IMSoP]

Rowan Tommins [IMSoP]

1 year ago
On 05/03/2025 23:10, Rowan Tommins [IMSoP] wrote:
> This is roughly what happened with Closures themselves in PHP: first, > decide that "$foo = function(){};" will be valid syntax, and define > Closure as the type of $foo; then over time, add additional behaviour > to the Closure class, the ability to add __invoke() hooks on other > classes, etc
Sorry to double-post, but Generators are probably a better example: you can write "$foo = yield $bar;" and there are well-defined semantics; on the outside of the function, we represent the state as a Generator object, and make it implement Iterable to explain how foreach() works; but on the inside of the function, it's pure magic: $bar is passed into an invisible channel, an invisible continuation is created, and when it's resumed another invisible channel passes out a value for $foo.
-- Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
> Defining new syntax would encourage us to define a minimum top-level > behaviour, such as "inside an async{} block, these things are possible, > and these things are guaranteed to be true"
True. This is precisely the main reason not to change the syntax. The issue is not even about how many changes need to be made in the code, but rather about how many agreements need to be considered. Ed.

Rowan Tommins [IMSoP]

1 year ago
On 06/03/2025 07:49, Edmond Dantes wrote:
> > Defining new syntax would encourage us to define a minimum top-level > > behaviour, such as "inside an async{} block, these things are possible, > > and these things are guaranteed to be true" > > True. This is precisely the main reason not to change the syntax. The > issue is not even about how many changes need to be made in the code, > but rather about how many agreements need to be considered.
Quite the opposite: with a function-and-object approach everything needs a name, an API, and a way of being described in relation to how the language already works. In a syntax-and-semantics approach, we only need to describe the things people actually need. The generator implementation doesn't have a name or API for where the value on the right of a "yield" goes, or where the value on its left comes from; we just describe the behaviour: values passed to yield somehow end up in the calling scope's Generator object, and values passed to that object somehow end up back at the yield statement. We don't have to define the API for a GeneratorContext object, and the semantics of what happens when users pass it around and store it in different scopes. In the same way, do we actually need to design what an "async context" looks like to the user? Do we actually want the user to be able to have access to two (nested) async contexts at once, and choose which one to spawn a task into? Or would we prefer, at least in the minimum implementation, to say "when you spawn a task, it spawns in the current async context, and if there is no current async context, an error is thrown"?
-- Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
> In a syntax-and-semantics approach, we only need to describe the things
people actually need. There is no doubt that syntax provides the programmer with a clear tool for expressing intent.
> In the same way, do we actually need to design what an "async context"
looks like to the user? Its implementation is more about deciding which paradigms we want to support. If we want to support global services that require local state within a coroutine, then they need a context. If there are no global "impure" services (i.e., those maintaining state within a coroutine), then a context may not be necessary. The first paradigm is not applicable to pure multitasking—almost all programming languages (as far as I know) have abandoned it in favor of ownership/memory passing. However, in PHP, it is popular. For example, PHP has functions for working with HTTP. One of them writes the last received headers into a "global" variable, and another function allows retrieving them. This is where a context is needed. Or, for instance, when a request is made inside a coroutine, the service that handles socket interactions under the hood must: 1. Retrieve a socket from the connection pool. 2. Place the socket in the coroutine’s context for as long as it is needed. However, this same scenario could be implemented more elegantly if PHP code explicitly used an object like "Connection" or "Transaction" and retrieved it from the pool. In that case, a context would not be needed. Thus, the only question is: do we need to maintain state between function/method calls within a coroutine?
> Do we actually want the user to be able to have access to two (nested)
async contexts at once, and choose which one to spawn a task into? If we discard the Go model, where the programmer decides what to do and which foot to shoot themselves in, and instead use parent-child coroutines, then such a function breaks this rule. This means it should not exist, as its presence increases system complexity. However, in the parent-child model, there is a case where a coroutine needs to be created in a different context. For example: - A request to reset a password arrives at the server. - The API creates a coroutine in a separate context from the request to send an email. - The API returns a 201 response. In this case, a special API is needed to accomplish this. The downside of any strict semantics is the presence of exceptional cases. However, such cases should be rare. If they are not, then the parent-child model is not suitable. To resolve this issue, we need to know the opinions of framework maintainers. They should say either: *Yes, this approach will reduce the amount of code*, or *No, it will increase the codebase*, or *We don't care, do as you like* :)

Rowan Tommins [IMSoP]

1 year ago
On 06/03/2025 11:31, Edmond Dantes wrote:
> For example, PHP has functions for working with HTTP. One of them > writes the last received headers into a "global" variable, and another > function allows retrieving them. This is where a context is needed.
OK, let's dig into this case: what is the actual problem, and what does an async design need to provide so that it can be solved. As far as I know, all current SAPIs follow one of two patterns: 1) The traditional "shared nothing" approach: each request is launched in a new process or thread, and all global state is isolated to that request. 2) The explicit injection approach: the request and response are represented as objects, and the user must pass those objects around to where they are needed. Notably, 2 can be emulated on top of 1, but not vice versa, and this is exactly what a lot of modern applications and frameworks do: they take the SAPI's global state, and wrap it in injected objects (e.g. PSR-7 ServerRequestInterface and ServerResponseInterface). Code written that way will work fine on a SAPI that spawns a fiber for each request, so there's no problem for us to solve there. At the other extreme are frameworks and applications that access the global state directly throughout - making heavy use of superglobal, global, and static variables; directly outputting using echo/print, etc. Those will break in a fiber-based SAPI, but as far as I can see, there's nothing the async design can do to fix that. In the middle, there are some applications we *might* be able to help: they rely on global state, but wrap it in global functions or static methods which could be replaced with some magic from the async implementation. So our problem statement is: - given a function that takes no request-specific input, and is expected to return request-specific state (e.g. function get_query_string_param(string $name): ?string) - and, given a SAPI that spawns a fiber for each request - how do we adjust the implementation of the function, without changing its signature? Things we don't need to define: - how the SAPI works - how the data is structured inside the function Non-solutions: - refactoring the application to pass around a Context object - if we're willing to do that, we can just pass around a PSR-7 RequestInterface instead, and the problem goes away Minimal solution: - a way to get an integer or string, which the function can use to partition its data Usage example: function get_query_string_param(string $name): ?string {     global $request_data; // in a shared-nothing SAPI, this is per-request; but in a fiber-based one, it's shared between requests     $request_data_partition = $request_data[ Fiber::getCurrent()->getId() ]; // this line makes the function work under concurrent SAPIs     return $request_data_partition['query_string'][$name]; // this line is basically unchanged from the original application } Limitation: - if the SAPI spawns a fiber for the request, but that fiber then spawns child fibers, the function won't find the right partition Minimal solution: - track and expose the "parent" of each fiber Usage example: function get_query_string_param(string $name): ?string {     global $request_data;     // Traverse until we find the ID we've stored data against in our request bootstrapping code     $fiber = Fiber::getCurrent();     while ( ! isset($request_data[ $fiber->getId() ] ) {         $fiber = $fiber->getParent();     }     $request_data_partition = $request_data[ $fiber->getId() ];     return $request_data_partition['query_string'][$name]; } Obviously, this isn't the only solution, but it is sufficient for this problem. As a first pass, it saves us bikeshedding exactly what methods an Async\Context class should have, because that whole class can be added later, or just implemented in userland. If we strip down the solution initially, we can concentrate on the fundamental design - things like "Fibers have parents", and what that implies for how they're started and used.
-- Rowan Tommins [IMSoP]

Larry Garfield

1 year ago
On Wed, Mar 5, 2025, at 3:10 PM, Edmond Dantes wrote:
>> No, just those functions/objects that necessarily involve running async control commands. Most wouldn't. >> They would just silently context switch when they hit an IO operation (which as noted above is transparency supported, which is what makes this >> work) and otherwise behave the same. > > So it's something more like Go or Python. > >> >> $val = async(function(AsyncContext $ctx) use ($stuff, $fn) { >> $result = []; >> foreach ($stuff as $item) { >> $result[] = $ctx->run($fn); >>} >> >> // We block/wait here until all subtasks are complete, then the async() call returns this value. >> return $result; >> }); > > Do I understand correctly that at the point `$val = > async(function(AsyncContext $ctx) use ($stuff, $fn)` execution stops > until everything inside is completed?
Correct. By the time $val is populated, all fibers/coroutines/tasks started inside that block have completed and closed, guaranteed. If an exception was thrown or something else went wrong, then by the time the exception escapes the asnc{} block, all fibers inside it are done and closed, guaranteed. (If there's another async {} block further up the stack somewhere, there may still be other background fibers running, but anything created inside that block is guaranteed done.)
> If so, let me introduce a second semantic option (for now, I'll remove > the context and focus only on the function). > > ```php > $url1 = 'https://domain1.com/'; > $url2 = 'https://domain2.com/'; > > $url_handle = fn(string $url) => file_get_contents($url); > > $res = Async\start(function() use ($url1, $url2, $url_handle) { > $res1 = Async\run($url_handle, $url1); > $res2 = Async\run($url_handle, $url2); > > Async\run(fn() => sleep(5)); > > // some logic here > > return $merged_result; > }); > ``` > > What's Happening Here: > > 1. After calling `$res = Async\start()`, the code waits until the > entire block completes. > 2. Inside `Async\start`, the code waits for all nested coroutines to > finish. > 3. If a coroutine has other nested coroutines, the same rule applies. > Rules Inside an Asynchronous Block: > > 1. I/O functions do not block coroutines within the block. > 2. Creating a new `Fiber` is not allowed — an exception will be > thrown: you cannot use `Fiber`. > 3. Unhandled exceptions will be thrown at the point of `$res = > Async\start()`. > Coroutine Cancellation Rules: > > Canceling a coroutine cancels it and all its child coroutines (this > cannot be bypassed unless the coroutine is created in a different > context). > > How does this option sound to you?
We can quibble on the details and spelling, but I think the overall logic is sound. One key question, if we disallow explicitly creating Fibers inside an async block, can a Fiber be created outside of it and not block async, or would that also be excluded? Viz, this is illegal: async { $f = new Fiber(some_func(...)); } But would this also be illegal? $f = new Fiber(some_func(...)); $f->start(); async { do_stuff(); }
> Essentially, this is Kotlin, but it should also resemble Python. > However, unlike Kotlin, there are no special language constructs > here—code blocks naturally serve that role. Of course, syntactic sugar > can be added later for better readability.
My brief foray into Kotlin in a previous job didn't get as far as coroutines, so I will take your word from it. From a very cursory glance at the documentation, I think runBlocking {} is approximately what I am describing, yes. The various other block types I don't know are necessary.
> And if you like this, I have good news: there are no implementation > issues at this level. > > In terms of semantic elegance, the only thing that bothers me is that > `return` behavior is slightly altered — meaning the actual "return" > won’t happen until all child functions complete. This isn’t very good, > and Kotlin’s style would fit better here.
I'm not sure I follow. The main guarantee we want is that "once you pass this }, all fibers/coroutines have ended, count on it." Do you mean something like this? async $ctx { $ctx->run(foo(...)); $ctx->run(bar(...)); // This return statement blocks until foo() and bar() complete. return "all done"; } That doesn't seem any weirder than return and finally{} blocks. :-) (Note that we can and should consider if async {} makes sense to have its own catch and finally blocks built in.)
> But on the other hand — can we live with this?
This seems far closer to something I'd support than the current RFC, yes.
>> I cannot speak to JS Symbols as I haven't used them. >> I am just vhemently opposed to globals, no matter how many layers they're wrapped in. :-) Most uses could be replaced by proper DI or partial application. > > You won’t be able to use DI because you have only *one service > (instance of class)* for the entire application, not a separate service > for each coroutine. This service is shared across the application and > can be called from any coroutine. As a result, the service needs memory > slots to store or retrieve data. DI is a mechanism used once during > service initialization, not every time a method is called.
Not true. DI doesn't imply singleton objects. Most good DI *containers* default to singleton objects, as they should, but for example Laravel's container does not. You have to opt-in to singleton behavior. (I think that's a terrible design, but it's still DI.) DI just means "a scope gets the stuff it needs given to it, it never asks for it." How that stuff is passed in is, deliberately, undefined. A DI container is but one way. In Crell/Serde, I actually use "runner objects" a lot. I have an example here: https://presentations.garfieldtech.com/slides-serialization/longhornphp2023/#/7/4/3 That is still dependency injection, because ThingRunner is still taking all of its dependencies via the constructor. And being readonly, it's still immutable-friendly. That's the sort of thing I'm thinking of here for the async context. To spitball again: class ClientManager { public function __construct(string $base) {} public function client(AsyncContext $ctx) { return new HttpClient($this->base, $ctx); } } class HttpClient { public function __construct(private string $base, private AsyncContext $ctx) {} public function get(string $path) { $this->ctx->defer(fn() => print "Read $path\n"); return $this->ctx->run(fn() => file_get_contents($this->base . $path)); } } $manager = $container->get(ClientManager::class); async $ctx { $client = $manager->client($ctx); $client->get('/foo'); $client->get('/bar'); } // We don't get here until all file_get_contents() calls are complete. // The deferred functions all get called right here. // There is no no async happening anymore. print "Done"; I'm pretty sure the return values are all messed up there, but hopefully you get the idea. Now HttpClient has a fully injected context that controls what async scope it's working in. The same class can be used in a bunch of different async blocks, each with their own context. You can even mock AsyncContext for testing purposes just like any other constructor argument. And not a global function or variable in sight! :-) --Larry Garfield

Edmond Dantes

1 year ago
> One key question, if we disallow explicitly creating Fibers inside an
async block,
> can a Fiber be created outside of it and not block async, or would that
also be excluded? Viz, this is illegal:
>
Creating a `Fiber` outside of an asynchronous block is allowed; this ensures backward compatibility. According to the logic integrity rule, an asynchronous block cannot be created inside a Fiber. This is a correct statement. However, if the asynchronous block blocks execution, then it does not matter whether a Fiber was created or not, because it will not be possible to switch it in any way. So, the answer to your question is: yes, such code is legal, but the Fiber will not be usable for switching. In other words, Fiber and an asynchronous block are mutually exclusive. Only one of them can be used at a time: either Fiber + Revolt or an asynchronous block. Of course, this is not an elegant solution, as it adds one more rule to the language, making it more complex. However, from a legacy perspective, it seems like a minimal scar. (to All: Please leave your opinion if you are reading this )
> // This return statement blocks until foo() and bar() complete.
Yes, *that's correct*. That's exactly what I mean. Of course, under the hood, return will execute immediately if the coroutine is not waiting for anything. However, the Scheduler will store its result and pause it until the child coroutines finish their work. In essence, this follows the parent-child coroutine pattern, where they are always linked. The downside is that it requires more code inside the implementation, and some people might accuse us of a paternalistic approach. :)
> > should consider if async {} makes sense to have its own catch and finally
blocks built in.)
>
We can use the approach from the RFC to catch exceptions from child coroutines: explicit waiting, which creates a handover point for exceptions. Alternatively, a separate handler like Context::catch() could be introduced, which can be defined at the beginning of the coroutine. Or both approaches could be supported. There's definitely something to think about here.
> > That is still dependency injection, because ThingRunner is still taking
all of its dependencies via the constructor. And being readonly, it's still immutable-friendly.
>
Yeah, so basically, you're creating the service again and again for each coroutine if the coroutine needs to use it. This is a good solution in the context of multitasking, but it loses in terms of performance and memory, as well as complexity and code size, because it requires more factory classes. The main advantage of *LongRunning* is initializing once and using it multiple times. On the other hand, this approach explicitly manages memory, ensuring that all objects are created within the coroutine's context rather than in the global context. Ah, now I see how much you dislike global state! :) However, in a scenario where a web server handles many similar requests, "global state" might not necessarily win in terms of speed but rather due to the simplicity of implementation and the overall maintenance cost of the code. (I know that in programming, there is an entire camp of immutability advocates who preach that their approach is the key remedy for errors.) I would support both paradigms, especially since it doesn’t cost much. A coroutine will own its internal context anyway, and this context will be carried along with it, even across threads. How to use this context is up to the programmer to decide. But at the same time, I will try to make the pattern you described fit seamlessly into this logic. Ed.

Daniil Gentili

1 year ago
> Of course, this is not an elegant solution, as it adds one more rule to the language, making it more complex. However, from a legacy perspective, it seems like a minimal scar. > (to All: Please leave your opinion if you are reading this ) >
Larry’s approach seems like a horrible idea to me: it increases complexity, prevents easy migration of existing code to an asynchronous model and is incredibly verbose for no good reason. The arguments mentioned in https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ are not good arguments at all, as they essentially propose explicitly reducing concurrency (by allowing it only within async blocks) or making it harder to use by forcing users to pass around contexts (which is even worse than function colouring https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). This (supposedly) reduces issues with resource contention/race conditions: sure, if you don’t use concurrency or severely limit it, you will have less issues with race conditions, but that’s not an argument in favour of nurseries, that’s an argument against concurrency. Race conditions and deadlocks are possible either way when using concurrency, and the way to avoid them is to introduce synchronisation primitives (locks, mutexes similar to the ones in https://github.com/amphp/sync/, or lockfree solutions like actors, which I am a heavy user of), not bloating signatures by forcing users to pass around contexts, reducing concurrency and completely disallowing global state. Golang is the perfect example of a language that does colourless, (mostly) contextless concurrency without the need for coloured (async/await keywords) functions and other complications. Race conditions are deadlocks are avoided, like in any concurrent model, by using appropriate synchronisation primitives, and by communicating with channels (actor model) instead of sharing memory, where appropriate. Side note, I *very* much like the current approach of implicit cancellations, because they even remove the need to pass contexts to make use of cancellations, like in golang or amphp (though the RFC could use some further work regarding cancellation inheritance between fibers, but that’s a minor issue).
> Yeah, so basically, you're creating the service again and again for each coroutine if the coroutine needs to use it. This is a good solution in the context of multitasking, but it loses in terms of performance and memory, as well as complexity and code size, because it requires more factory classes. >
^ this Regarding backwards compatibility (especially with revolt), since I also briefly considered submitting an async RFC and thought about it a bit, I can suggest exposing an event loop interface like https://github.com/revoltphp/event-loop/blob/main/src/EventLoop.php, which would allow userland event loop implementations to simply switch to using the native event loop as backend (this’ll be especially simple to do for which is the main user of fibers, revolt, since the current implementation is clearly inspired by revolt’s event loop). Essentially, the only thing that’s needed for backwards-compatibility in most cases is an API that can be used to register onWritable, onReadable callbacks for streams and a way to register delayed (delay) tasks, to completely remove the need to invoke stream_select. I’d recommend chatting with Aaron to further discuss backwards compatibility and the overall RFC: I’ve already pinged him, he’ll chime in once he has more time to read the RFC. ~~~ To Edmond, as someone who submitted RFCs before: stand your ground, try not to listen too much to what people propose in this list, especially if it’s regarding radical changes like Larry's; avoid bloating the RFC with proposals that you do not really agree with. Regards, Daniil Gentili — Daniil Gentili - Senior software engineer Portfolio: https://daniil.it <https://daniil.it/> Telegram: https://t.me/danogentili

Edmond Dantes

1 year ago
Hello, Daniil.
> Essentially, the only thing that’s needed for backwards-compatibility in
most cases is an API that can be used to register onWritable,
> onReadable callbacks for streams and a way to register delayed (delay)
tasks, to completely remove the need to invoke stream_select. Thank you for this point. It seems I was mistaken in thinking that there is a Scheduler inside Revolt. Of course, if we're only talking about the EventLoop, maintaining compatibility won't be an issue at all.
> I’d recommend chatting with Aaron to further discuss backwards
compatibility and the overall RFC: I’ve already pinged him, he’ll chime in once he has more time to read the RFC. That would be really cool.
> To Edmond, as someone who submitted RFCs before: stand your ground, try
not to listen too much to what people propose in this list,
> especially if it’s regarding radical changes like Larry's; avoid bloating
the RFC with proposals that you do not really agree with. Actually, I agree in many ways. In programming, there's an eternal struggle between abstraction and implementation, between strict rules and flexibility, between paternalism where the language makes decisions for you and freedom. Each of these traits is beneficial in certain scenarios. The most important thing is to understand whether it will be beneficial for PHP scenarios. This is the main goal of this RFC stage. That's why I would really like to hear the voices of those who create PHP's code infrastructure. I mean, Symfony, Laravel, etc. Thanks! Ed.

Larry Garfield

1 year ago
On Thu, Mar 6, 2025, at 2:52 AM, Edmond Dantes wrote:
>> One key question, if we disallow explicitly creating Fibers inside an async block, >> can a Fiber be created outside of it and not block async, or would that also be excluded? Viz, this is illegal: >> > Creating a `Fiber` outside of an asynchronous block is allowed; this > ensures backward compatibility. > According to the logic integrity rule, an asynchronous block cannot be > created inside a Fiber. This is a correct statement. > > However, if the asynchronous block blocks execution, then it does not > matter whether a Fiber was created or not, because it will not be > possible to switch it in any way. > So, the answer to your question is: yes, such code is legal, but the > Fiber will not be usable for switching. > > In other words, Fiber and an asynchronous block are mutually exclusive. > Only one of them can be used at a time: either Fiber + Revolt or an > asynchronous block. > > Of course, this is not an elegant solution, as it adds one more rule to > the language, making it more complex. However, from a legacy > perspective, it seems like a minimal scar. > > (to All: Please leave your opinion if you are reading this )
This seems like a reasonable approach to me, given the current state. At any give time, you can have "manual" or "automatic" handling in use, but one has to completely finish before you can start using the other. Whether we should remove the "manual" access in the future becomes a question for the future.
>> // This return statement blocks until foo() and bar() complete. > > Yes, *that's correct*. That's exactly what I mean. > > Of course, under the hood, `return` will execute immediately if the > coroutine is not waiting for anything. However, the Scheduler will > store its result and pause it until the child coroutines finish their > work. > > In essence, this follows the parent-child coroutine pattern, where they > are always linked. The downside is that it requires more code inside > the implementation, and some people might accuse us of a paternalistic > approach. :)
See, what you call "paternalistic" I say is "basic good usability." Affordances are part of the design of everything. Good design means making doing the right thing easy and the wrong thing hard, preferably impossible. (Eg, why 120v and 220v outlets have incompatible plugs, to use the classic example.) I am a strong support of correct by construction / make invalid states unrepresentable / type-driven development, or whatever it's called this week. And history has demonstrated that humans simply cannot be trusted to manually handle synchronization safely, just like they cannot be trusted to manually handle memory safely. :-) (That's why green threads et al exist.)
>> That is still dependency injection, because ThingRunner is still taking all of its dependencies via the constructor. And being readonly, it's still immutable-friendly. >> > > Yeah, so basically, you're creating the service again and again for > each coroutine if the coroutine needs to use it. This is a good > solution in the context of multitasking, but it loses in terms of > performance and memory, as well as complexity and code size, because it > requires more factory classes.
Not necessarily. It depends on what all you're doing when creating those objects. It can be quite fast. Plus, if you want a simpler approach, just pass the context directly: async $ctx { $ctx->run($httpClient->runAsync($ctx, $url)); } It's just a parameter to pass. How you pass it is up to you. It is literally the same argument for "pass the DB connection into the constructor, don't call a static method to get it" or "pass in the current user object to the method, don't call a global function to get it." These are decades-old discussions with known solved problems, which all boil down to "pass things explicitly." To quote someone on FP: "The benefit of functional programming is it makes data flow explicit. The downside is it sometimes painfully explicit." I am far happier with explicit that is occasionally annoyingly so, and building tools and syntax to reduce that annoyance, than having implicit data just floating around in the ether around me and praying it's what I expect it to be.
> The main advantage of *LongRunning* is initializing once and using it > multiple times. On the other hand, this approach explicitly manages > memory, ensuring that all objects are created within the coroutine's > context rather than in the global context.
As above, in simpler cases you can just make the context a boring old function parameter, in which case the perf overhead is unmesurable.
> Ah, now I see how much you dislike global state! :)
It is the root of all evil.
> However, in a scenario where a web server handles many similar > requests, "global state" might not necessarily win in terms of speed > but rather due to the simplicity of implementation and the overall > maintenance cost of the code. (I know that in programming, there is an > entire camp of immutability advocates who preach that their approach is > the key remedy for errors.) > > I would support both paradigms, especially since it doesn’t cost much.
Depends on the cost you mean. If you have "system with strong guarantees" and "system with no guarantees" interacting, then you have a system with no guarantees. Plus the cost of devs having to think about two different APIs, one of which is unit testable and one of which isn't, or at least not easily. Do you have a concrete example of where the inconvenience of explicit context is sufficiently high to warrant an implicit global and all the impacts that has? --Larry Garfield

Edmond Dantes

1 year ago
> > See, what you call "paternalistic" I say is "basic good usability." > Affordances are part of the design of everything. Good design means
making doing the
>
If we worry about "intuitive usability", we should ban caching, finite state machines, and of course, concurrency. Parallelism? Not just ban it, but burn those who use it at the stake of the inquisition! :) In this context, the child-parent model has a flaw that directly contradicts intuitive usage. Let me remind you of the main rule: Default behavior: All child coroutines are canceled if the parent is canceled. Now, imagine a case where we need to create a coroutine not tied to the parent. To do this, we have to define a separate function or syntax. Such a coroutine is created to perform an action that must be completed, even if the parent coroutines are not fully executed. Typically, this is a critical action, like logging or sending a notification. This leads to an issue: * Ordinary actions use a function that the programmer always remembers. * Important actions require a separate function, which the programmer might forget. This is the dark side of any strict design when exceptions exist (and they almost always do). And the problem is bigger than it seems because: 1. The parent coroutine is created in Function A. 2. The child coroutine is created in Function B. 3. These functions are in different modules, written by different developers. Developer A implements a unique algorithm that cancels coroutine execution. This algorithm is logical and correct in the context of A. Developer B simply forgets that execution might be interrupted. And boom! We've just introduced a bug that will send the entire dev team on a wild goose chase. This is why the Go model (without parent-child links) is different: It makes chaining coroutines harder. But if you don’t need chains, it’s simpler. And whether you need chains or not is a separate question. Possible scenarios in PHP *Scenario 1* We need to generate a *report*, where data must be collected from multiple services. - We create *one coroutine per service*. - Wait for all of them to finish. - Generate the report. Parent-child model is ideal: If the *parent coroutine* is canceled, the *child coroutines* are meaningless as well. ------------------------------ *Scenario 2* *Web server.* The API receives a request to create a *certificate*. The algorithm: 1. *Check* if we can do it, then create a *DB record* stating that the user has a certificate. 2. *Send a Job* – notify other users who need to know about this event. 3. *Return the certificate URL* (a link with an ID). *Key requirement:* - *Heavy operations* (longer than *2-3 seconds*) should be performed *in a Job-Worker pool* to keep the server *responsive*. - Notifications are sent *as a separate Job* in *a separate coroutine*, which: - Can retry sending *twice if needed*. - Implements a *fallback mechanism*. - *Is NOT linked* to the request coroutine. ------------------------------ Which scenario is more likely for PHP?
> > To quote someone on FP: "The benefit of functional programming is it
makes data flow explicit. The downside is it sometimes painfully explicit."
>
If there is a nesting of 10 functions where parameters are passed explicitly, then the number of parameters in the top function will be equal to the sum of the parameters of all other functions, and the overall code coupling will be 100%. Parameters can be grouped into objects (structures), thus reducing this problem. However, creating additional objects leads to the temptation to shove a parameter into the first available object because thinking about composition is a difficult task. This means that such an approach either violates SOLID or increases design complexity. But usually, the worst-case scenario happens: developers happily violate both SOLID and design. :) I think these principles are more suitable for areas where design planning takes up 30-50% of the total development time and where such a time distribution is rational in relation to the project's success. At the same time, the initial requirements change extremely rarely. PHP operates under completely different conditions: "it was needed yesterday" :)
> > As above, in simpler cases you can just make the context a boring old
function parameter,
>
What if a service wants to store specific data in the context? As for directly passing the context into a function, the coroutine already owns the context, and it can be retrieved from it. This is a consequence of PHP having an abstraction that C/Rust lacks, allowing it to handle part of the dirty work on behalf of the programmer. It's the same as when you use $this when calling a method.
> > Do you have a concrete example of where the inconvenience of explicit
context is sufficiently high to warrant an implicit global and all the impacts that has?
>
The refactoring issue. There are five levels of nesting. At the fifth level, someone called an asynchronous function and created a context. Thirty days later, someone wanted to call an asynchronous function at the first level of nesting. And suddenly, it turns out that the context needs to be explicitly passed. And that's where the fun begins. :) --- Ed.

Rowan Tommins [IMSoP]

1 year ago
On 07/03/2025 09:24, Edmond Dantes wrote:
> Now, imagine a case where we need to create a coroutine not tied to > the parent. > To do this, we have to define a separate function or syntax. > > Such a coroutine is created to perform an action that must be completed, > even if the parent coroutines are not fully executed. > Typically, this is a critical action, like logging or sending a > notification. > > This leads to an issue: > > * Ordinary actions use a function that the programmer always remembers. > * Important actions require a separate function, which the programmer > might forget.
Let's assume we want to support this scenario; we could: a) Throw away all automatic resource management, and make it the user's responsibility to arrange for additional fibers to be cancelled when their "parent" is cancelled b) Create unmanaged fibers by default, but provide a simple mechanism to "attach" to a child/parent c) Provide automatic cleanup by default, but a simple mechanism to "disown" a child/parent (similar to Unix processes) d) Provide two separate-but-equal primitives for spawning coroutines, "run as child", and "run as top-level" Option (a) feels rather unappealing; it also implies that no "parent" relationship is available for things like context data. I think you agree that top-level fibers would be the less common case, so (b) seems awkward as well. Option (c) might look like this: async {     $child = asyncRun foo();     $bgTask = asyncRun bar();     $bgTask->detach(); } // foo() guaranteed to be completed or cancelled, bar() continuing as an independent fiber Or maybe the detach would be inside bar(), e.g. Fiber::getCurrent()->detach() Option (d) might look like this: async {     $child = asyncChild foo();     $bgTask = asyncDetached bar(); } // foo() guaranteed to be completed or cancelled, bar() continuing as an independent fiber (all names and syntax picked for fast illustration, not an exact proposal)
-- Rowan Tommins [IMSoP]

Rowan Tommins [IMSoP]

1 year ago
On 6 March 2025 19:07:34 GMT, Larry Garfield <larry@garfieldtech.com> wrote:
>It is literally the same argument for "pass the DB connection into the constructor, don't call a static method to get it" or "pass in the current user object to the method, don't call a global function to get it." These are decades-old discussions with known solved problems, which all boil down to "pass things explicitly."
I think the counterargument to this is that you wouldn't inject a service that implemented a while loop, or if statement. I'm not even sure what mocking a control flow primitive would mean. Similarly, we don't pass around objects representing the "try context" so that we can call "throw"as a method on them. I'm not aware of anybody complaining that they can't mock the throw statement as a consequence, or wanting to work with multiple "try contexts" at once and choose which one to throw into. A lexically scoped async{} statement feels like it could work similarly: the language primitive for "run this code in a new fiber" (and I think it should be a primitive, not a function or method) would look up the stack for an open async{} block, and that would be the "nursery" of the new fiber. [You may not like that name, but it's a lot less ambiguous than "context", which is being used for at least two different things in this discussion.] Arguably this is even needed to be "correct by construction" - if the user can pass around nurseries, they can create a child fiber that outlives its parent, or extend the lifetime of one nursery by storing a reference to it in a fiber owned by a different nursery. If all they can do is spawn a fiber in the currently active nursery, the child's lifetime guaranteed to be no longer than its parent, and that lifetime is defined rigidly in the source code. Rowan Tommins [IMSoP]

Larry Garfield

1 year ago
On Fri, Mar 7, 2025, at 3:39 AM, Rowan Tommins [IMSoP] wrote:
> On 6 March 2025 19:07:34 GMT, Larry Garfield <larry@garfieldtech.com> wrote: >>It is literally the same argument for "pass the DB connection into the constructor, don't call a static method to get it" or "pass in the current user object to the method, don't call a global function to get it." These are decades-old discussions with known solved problems, which all boil down to "pass things explicitly." > > I think the counterargument to this is that you wouldn't inject a > service that implemented a while loop, or if statement. I'm not even > sure what mocking a control flow primitive would mean. > > Similarly, we don't pass around objects representing the "try context" > so that we can call "throw"as a method on them. I'm not aware of > anybody complaining that they can't mock the throw statement as a > consequence, or wanting to work with multiple "try contexts" at once > and choose which one to throw into. > > A lexically scoped async{} statement feels like it could work > similarly: the language primitive for "run this code in a new fiber" > (and I think it should be a primitive, not a function or method) would > look up the stack for an open async{} block, and that would be the > "nursery" of the new fiber. [You may not like that name, but it's a lot > less ambiguous than "context", which is being used for at least two > different things in this discussion.] > > Arguably this is even needed to be "correct by construction" - if the > user can pass around nurseries, they can create a child fiber that > outlives its parent, or extend the lifetime of one nursery by storing a > reference to it in a fiber owned by a different nursery. If all they > can do is spawn a fiber in the currently active nursery, the child's > lifetime guaranteed to be no longer than its parent, and that lifetime > is defined rigidly in the source code. > > Rowan Tommins > [IMSoP]
Since I think better in code, if using try-catch as a model, that would lead to something like: function foo(int $x): int { // if foo() is called inside an async block, this is non-blocking. // if it's called outside an async block, it's blocking. syslog(__FUNCTION__); return 1; } function bar(int $x): int { return $x + 1; // Just a boring function like always. } function baz(int $x): int { // Because this is called here, baz() MUST only be called from // inside a nested async block. Doing otherwise cause a fatal at runtime. spawn foo($x); } async { // Starts a nursery $res1 = spawn foo(5); // Spawns new Fiber that runs foo(). $res2 = spawn bar(3); // A second fiber. $res3 = spawn baz(3); // A Third fiber. // merge results somehow return $combinedResult; } // We block here until everything spawned inside this async block finishes. spawn bar(3); // This is called outside of an async() block, so it just crashes the program (like an uncaught exception). Is that what you're suggesting? If so, I'd have to think it through a bit more to see what guarantees that does[n't] provide. It might work. (I deliberately used spawn instead of "await" to avoid the mental association with JS async/await.) My biggest issue is that this is starting to feel like colored functions, even if partially transparent. --- Another point worth mentioning: I get the impression that there are two very different mental models of when/why one would use async that are floating around in this thread, which lead to two different sets of conclusions. 1. Async in the small: Like the reporting example, "fan out" a set of tasks, and bring them back together quickly before continuing in an otherwise mostly sync PHP-FPM process. All the data is still part of one user request, so we still have "shared nothing." 2. Async in the large: A long running server like Node.js, ReactPHP, etc. Multiplexing several user requests into one OS process via async on the IO points. Basically the entire application has a giant async {} wrapped around it. Neither of these is a bad use case, and they're not mutually exclusive, but they do lead to different priorities. I freely admit my bias is towards Type 1, while it sounds like Edmond is coming from a Type 2 perspective. Not a criticism, just flagging it as something that we should be aware of. --Larry Garfield

Daniil Gentili

1 year ago
> Of course, this is not an elegant solution, as it adds one more rule to the language, making it more complex. However, from a legacy perspective, it seems like a minimal scar. > (to All: Please leave your opinion if you are reading this ) >
Larry’s approach seems like a horrible idea to me: it increases complexity, prevents easy migration of existing code to an asynchronous model and is incredibly verbose for no good reason. The arguments mentioned in https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ are not good arguments at all, as they essentially propose explicitly reducing concurrency (by allowing it only within async blocks) or making it harder to use by forcing users to pass around contexts (which is even worse than function colouring https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). This (supposedly) reduces issues with resource contention/race conditions: sure, if you don’t use concurrency or severely limit it, you will have less issues with race conditions, but that’s not an argument in favour of nurseries, that’s an argument against concurrency. Race conditions and deadlocks are possible either way when using concurrency, and the way to avoid them is to introduce synchronisation primitives (locks, mutexes similar to the ones in https://github.com/amphp/sync/, or lockfree solutions like actors, which I am a heavy user of), not bloating signatures by forcing users to pass around contexts, reducing concurrency and completely disallowing global state. Golang is the perfect example of a language that does colourless, (mostly) contextless concurrency without the need for coloured (async/await keywords) functions and other complications. Race conditions are deadlocks are avoided, like in any concurrent model, by using appropriate synchronisation primitives, and by communicating with channels (actor model) instead of sharing memory, where appropriate. Side note, I *very* much like the current approach of implicit cancellations, because they even remove the need to pass contexts to make use of cancellations, like in golang or amphp (though the RFC could use some further work regarding cancellation inheritance between fibers, but that’s a minor issue).
> Yeah, so basically, you're creating the service again and again for each coroutine if the coroutine needs to use it. This is a good solution in the context of multitasking, but it loses in terms of performance and memory, as well as complexity and code size, because it requires more factory classes. >
^ this Regarding backwards compatibility (especially with revolt), since I also briefly considered submitting an async RFC and thought about it a bit, I can suggest exposing an event loop interface like https://github.com/revoltphp/event-loop/blob/main/src/EventLoop.php, which would allow userland event loop implementations to simply switch to using the native event loop as backend (this’ll be especially simple to do for which is the main user of fibers, revolt, since the current implementation is clearly inspired by revolt’s event loop). Essentially, the only thing that’s needed for backwards-compatibility in most cases is an API that can be used to register onWritable, onReadable callbacks for streams and a way to register delayed (delay) tasks, to completely remove the need to invoke stream_select. I’d recommend chatting with Aaron to further discuss backwards compatibility and the overall RFC: I’ve already pinged him, he’ll chime in once he has more time to read the RFC. ~~~ To Edmond, as someone who submitted RFCs before: stand your ground, try not to listen too much to what people propose in this list, especially if it’s regarding radical changes like Larry's; avoid bloating the RFC with proposals that you do not really agree with. Regards, Daniil Gentili — Daniil Gentili - Senior software engineer Portfolio: https://daniil.it <https://daniil.it/> Telegram: https://t.me/danogentili

Jakub Zelenka

1 year ago
Hi,
> https://wiki.php.net/rfc/true_async > > I believe this version is not perfect and requires analysis. And I > strongly believe that things like this shouldn't be developed in isolation. > So, if you think any important (or even minor) aspects have been > overlooked, please bring them to attention. >
I thought about this quite a bit and I think we should first try to clarify the primary design that we want to go for. What I mean is whether we would like to ever support a true concurrency (threads) in it. If we think it would be worth it (even thought it wouldn't be initially supported), then we should take it into the account from the beginning and add restrictions to prevent race conditions. It means it should probably disallow global (e.g. global $var;) variables or at least make them context specific as well as disallowing object sharing. I think PHP users should not deal with synchronization primitives. Basically what I want to say is that multithreading should not be just something mentioned in the future scope but the whole design should be done in a way that will make sure everything will work fine for users. Ideally also having some simplified implementation that will verify it. I also agree that the scope is currently too big. It should be reduced to the absolute minimum and just show what's possible. It's great to have a proof of concept for that but the initial proposal should be mainly about the design and introducing the core components. Regards, Jakub

Edmond Dantes

1 year ago
Hello, Jakub.
> > I thought about this quite a bit and I think we should first try to
clarify the primary design that we want to go for.
> What I mean is whether we would like to ever support a true concurrency
(threads) in it.
> If we think it would be worth it (even thought it wouldn't be initially
supported), then we should take it into the account from the beginning and add restrictions to prevent race conditions.
>
If you mean multitasking, i.e., executing coroutines in different OS threads, then this feature is far beyond the scope of this RFC and would require significant changes to the PHP core (Memory manager first). And even if we imagine that such changes are made, eliminating data races without breaking the language is, to put it mildly, a questionable task from the current perspective. Although this RFC raises the question of whether a concurrent version without multitasking is worth implementing at all, my opinion is positive. For PHP, this could be sufficient as a language primarily used in the context of asynchronous I/O, whereas a multitasking version may never happen. I will likely pose this as a direct question in the final part of this RFC. Thanks! Ed.

Jakub Zelenka

1 year ago
Hi,
> > > I thought about this quite a bit and I think we should first try to > clarify the primary design that we want to go for. > > What I mean is whether we would like to ever support a true concurrency > (threads) in it. > > If we think it would be worth it (even thought it wouldn't be initially > supported), then we should take it into the account from the beginning and > add restrictions to prevent race conditions. > > > > If you mean multitasking, i.e., executing coroutines in different OS > threads, then this feature is far beyond the scope of this RFC and would > require significant changes to the PHP core (Memory manager first). >
You might want to look to parallel extension as it's already dealing with that and mostly works - of course combination with coroutines will certainly complicate it but the point is that memory is not shared.
> And even if we imagine that such changes are made, eliminating data races > without breaking the language is, to put it mildly, a questionable task > from the current perspective. >
That's exactly what I meant is to make sure that there won't be any data races - it means use only channel communication (more below).
> > Although this RFC raises the question of whether a concurrent version > without multitasking is worth implementing at all, my opinion is positive. > For PHP, this could be sufficient as a language primarily used in the > context of asynchronous I/O, whereas a multitasking version may never > happen. > >
I didn't really mean to introduce it as part of this RFC. What I meant is to design the API so there is still possibility to add it in the future without risking various race condition in the code. It means primarily to put certain restrictions that will prevent it like limited access to global and passing anything by reference (including objects) to the running tasks. Regards Jakub

Edmond Dantes

1 year ago
> You might want to look to parallel extension as it's already dealing > with that and mostly works - of course combination with coroutines will
certainly complicate it but the point is that memory is not shared. Do you mean this extension: https://www.php.net/manual/en/book.parallel.php? Yes, I studied it before starting the development of True Async. My very first goal was to enable, if not coroutine execution across threads, then at least interaction, because the same thing is already possible with Swoole. However, parallel does not provide multitasking and will not be able to in the future. The best it can offer is interaction via Channel between two different threads, which will be useful for job processing and built-in web servers. And here's the frustrating part. It turns out that parallel has to copy PHP bytecode for correct execution in another thread. This means that not only does the memory manager need to be replaced with a multi-threaded version, but the virtual machine itself must also be refactored. For PHP to work correctly in multiple threads with context switching, it will be necessary to find a way to rewrite all the code that reads/writes global variables. (Where TLS macros are used, this shouldn't be too difficult. But is that the case everywhere?) This applies to all extensions, both built-in and third-party. Such a language update would create an "extension vacuum." When a new version is released, many extensions will become unavailable due to the need to adapt to the new multitasking model.
> I didn't really mean to introduce it as part of this RFC. > What I meant is to design the API so there is still possibility to add it
in the future without risking various race condition in the code.
> It means primarily to put certain restrictions that will prevent it like
limited access to global and passing anything by
> reference (including objects) to the running tasks.
Primitives like *Context* are unlikely to be the main issue for multitasking. The main problem will be the code that has been developed for many years with single-threaded execution in mind. This is another factor that raises doubts about the rationale for introducing real multitasking in PHP. If we are talking about a model similar to Python’s, the current RFC already works with it, as a separate thread is used on Windows to wait for processes and send events to the PHP thread. Therefore, integrating this RFC with parallel is not an issue. It would be great to solve the bytecode problem in a way that allows it to be freely executed across different threads. This would enable running any closure as a coroutine in another OS thread and interacting through a channel. If you talk about this functionality, it does not block concurrency or current RFC. This capability should be considered as an additional feature that can be implemented later without modifying the existing primitives. While working on this RFC, I also considered finding a way to create SharedObject instances that could be passed between threads. However, I ultimately concluded that this solution would require changes to the memory manager, so these objects were not included in the final document. Ed.

Larry Garfield

1 year ago
On Wed, Mar 5, 2025, at 11:50 AM, Edmond Dantes wrote:
>> You might want to look to parallel extension as it's already dealing >> with that and mostly works - of course combination with coroutines will certainly complicate it but the point is that memory is not shared. > > Do you mean this extension: https://www.php.net/manual/en/book.parallel.php? > > Yes, I studied it before starting the development of True Async. My > very first goal was to enable, if not coroutine execution across > threads, then at least interaction, because the same thing is already > possible with Swoole.
*snip*
> Such a language update would create an "extension vacuum." When a new > version is released, many extensions will become unavailable due to the > need to adapt to the new multitasking model.
It would necessitate a major version release, certainly.
>> I didn't really mean to introduce it as part of this RFC. >> What I meant is to design the API so there is still possibility to add it in the future without risking various race condition in the code. >> It means primarily to put certain restrictions that will prevent it like limited access to global and passing anything by >> reference (including objects) to the running tasks. > > Primitives like *Context* are unlikely to be the main issue for > multitasking. The main problem will be the code that has been developed > for many years with single-threaded execution in mind. This is another > factor that raises doubts about the rationale for introducing real > multitasking in PHP.
I think the point is more that the concurrency primitives that are introduced (async block, async() function, whatever it is) should be designed in such a way that PHP could introduce multiple parallel threads in the future to run multiple async blocks simultaneously... without any impact on the *user* code. To reuse my earlier example: function parallel_map(iterable $it, Closure $fn) { $result = []; async $ctx { foreach ($it as $k => $v) { $result[$k] = $ctx->run($fn($v)); } } return $result; } Whether each run() invocation is handled by one thread switching between them or 3 threads switching between them is not something the above code should care about. Which means designing that API in such a way that I... don't need to care. Which probably means something like "no inter-fiber communication other than channels", as in Go. And thinking through what it means for the context object if it does have some kind of global property bag. (This is one reason I don't want one.) And it means there's no way to control threads directly from user-space. You just get async blocks, and that's it. This is an area that Go got pretty solidly right, and is worth emulating. The implications on C code of adding true-thread support in the future is a separate question; the async API should be built such that it *can* be a separate future question. --Larry Garfield

Edmond Dantes

1 year ago
Hello all. A few thoughts aloud about the emerging picture. ### Entry point into the asynchronous context Most likely, it should be implemented as a separate function (I haven't come up with a good name yet), with a unique name to ensure its behavior does not overlap with other operators. It has a unique property: it waits for the full completion of the event loop and the Scheduler. Inside the asynchronous context, `Fiber` is prohibited, and conversely, inside a `Fiber`, the asynchronous context is prohibited. ### The `async` operator The `async` (or *spawn*?) operator can be used as a shorthand for spawning a coroutine: ```php function my($param) {} // Operator used as a function call async my(1); or spawn my(1); // Operator used as a closure async { code }; // Since it's a closure, the `use` statement can be used without restrictions async use($var) { code }; // Returns a coroutine class instance $x = async use($var) { code }; ``` ### The `await` operator The `await` operator can be added to `async`, allowing explicit suspension of execution to wait for a result: ```php $x = await async use($var) { code }; ``` ## Context Manipulations I didn't like functions like `overrideContext`. They allow changing the context multiple times at any point in a function, which can lead to errors that are difficult to debug. This is a really bad approach. It is much better to declare the context at the time of coroutine invocation. With syntax, it might look like this: ```php async in $context use() {} async in $context myFun() async in $context->with("key", value) myFun() or spawn in $context ... ``` ## Thread The syntax spawn in/async in can be used not only in standard cases. ```php $coro = async in new ThreadContext() use($channel) { while() {} }; // This expression is also valid $coro = async in new $threadPool->borrowContext() use($channel) { while() {} }; ``` It is worth noting that $threadPool itself may be provided by an extension and not be a part of PHP. ## Unrelated Coroutines An additional way is needed to create a coroutine that is not bound to a parent. It's worth considering how to make this as clear and convenient as possible. Maybe as keyword: ```php async unbound ... ``` Of course, an `async child` modifier can be used. This is the inverse implementation, but I think it will not be used often. Making `unbound` a separate method is not very appealing at the moment because a programmer might forget to call it. They could forget the word `unbound`, and even more so a whole method. ## Context Operations ```php await $context; // Waits for all coroutines in the context $context.cancel(); // Cancels everything within the context ``` ## Flow I want to thank all the participants in the discussion. Thanks to your ideas, questions, and examples. A week ago, answering this question would have been impossible. If we add exception handling and graceful shutdown, and remove the new syntax by replacing it with an equivalent of 2-3 functions, we will get a fairly cohesive RFC that describes the high-level part without unnecessary details. Channels and even Future can be excluded from this RFC — everything except the coroutine class and context. Microtasks, of course, will remain. As a result, the RFC will be clean and compact, focusing solely on how coroutines and context work. Channels, Future, and iterators can be moved to a separate RFC dedicated specifically to primitives. Finally, after reviewing the high-level RFCs, we can return to the implementation — in other words, top-down. Given that the approximate structure of the lower level is already clear, discussing abstractions will remain practical and grounded. Just to clarify, I’m not planning to end the current discussion these are just intermediate thoughts. --- Ed.

Larry Garfield

1 year ago
On Sat, Mar 8, 2025, at 1:05 AM, Edmond Dantes wrote:
> Hello all. > > A few thoughts aloud about the emerging picture. > > ### Entry point into the asynchronous context > Most likely, it should be implemented as a separate function (I haven't > come up with a good name yet), with a unique name to ensure its > behavior does not overlap with other operators. It has a unique > property: it waits for the full completion of the event loop and the > Scheduler. > > Inside the asynchronous context, `Fiber` is prohibited, and conversely, > inside a `Fiber`, the asynchronous context is prohibited.
Yes.
> ### The `async` operator > The `async` (or *spawn*?) operator can be used as a shorthand for > spawning a coroutine:
This is incorrect. "Create an async bounded context playpen" (what I called "async" in my example) and "start a fiber/thread/task" (what I called "spawn") are two *separate* operations, and must remain so. create space for async stuff { start async task a(); start async task b(); } However those get spelled, they're necessarily separate things. If any creation of a new async task also creates a new async context, then we no longer have the ability to run multiple tasks in parallel in the same context. Which is, as I understand it, kinda the point. I also don't believe that an async bounded context necessarily needs to be a function, as doing so introduces a lot of extra complexity for the user when they need to manually "use" things. (Though perhaps sometimes we can have a shorthand for that; that comes later.) I am also still very much against allowing tasks to "detach". If a thread is allowed to escape its bounded context, then I can no longer rely on that context being bounded. It removes the very guarantee that we're trying to provide. There are better ways to handle "throwing off a long-running background task." (See below.) Edmond, correct me if I'm wrong here, but in practice, the *only* places that it makes sense to switch fibers are: 1. At an otherwise-blocking IO call. 2. In a very long running CPU task, where the task is easily broken up into logical pieces so that we can interleave it with shorter tasks in the same process. This is only really necessary when running a shared single process for multiple requests. And in this proposal, IO operations auto-switch between blocking and thread-sharing as appropriate. To be more concrete, let's consider specific use cases that should be addressed: 1. Multiplexing IO, within an otherwise sync context like PHP-FPM I predict that, in the near term, this will be the most common usage pattern. (Long term, who knows.) This one is easily solvable; it's basically par_map() and variations therein. // Creates a context in which async is allowed to happen. IO operations auto async $ctx = new AsyncContext() { $val1 = spawn task1(); $val2 = spawn task2(); // Do stuff with those values. } // We are absolutely certain nothing started in that block is still running. (I'm still unclear if $val1 and $val2 should be values or a Future object. Possibly the latter.) 4. Shared-process async server This is the ReactPHP/Swoole space. This... honestly gets kind of easy. Wrap the entire application in an async {} block. Boom. All IO is now async. <?php async { while (true) { $request = spawn listen_for_request(); spawn handle_request($request); } } Importantly, since IO is the primary switch point, and IO automatically deals with thread switching, my DB-query-heavy Repository object doesn't care if I'm doing this or not. If each $handler (controller, whatever) is written 100% sync, with lots of IO... it still works fine. 3. Set-and-forget background job This is the logger example, but probably also queue tasks, etc. This is where the request for detaching comes from. I would argue detaching is both the wrong approach, and an unnecessary one. Because you can send data to fibers from OTHER contexts... via channels. So rather than this: spawn detatch log('message'); // Who the hell knows when this will complete, or if it ever does. We have this: async { $logger = new AsyncLogger(); $channel = $logger->inputChannel(); spawn handler($logChannel); } function handler($logger) { async { while (true) { $request = spawn listen_for_request(); spawn handle_request($request, $logChannel); } // An exception could get us to here. } } function handle_request($request, $logChannel) { $logChannel->send($request->url()); // Do other complex stuff with the request. } This is probably not the ideal way to structure it in practice, but it should get the point across. The background logger fiber already exists in the parent async playpen. That's OK! We can send messages to it via a channel. It can keep running after the inner async block ends. The logger fiber doesn't need to be attached, because it was already attached to a parent playpen anyway! This means passing either a channel-enabled logger instance around (probably better for BC; this should be easy to do behind PSR-3) or the sending channel itself. I'm sure someone will object that is too much work. However, it is no more, or less, work than passing a PSR-3 logger to services today. And in practice "your DI container handles that, stop worrying" is a common and effective answer. An async-aware DI Container could have an Async-aware PSR-3 logger it passes to various services like any other boring PSR-3 instance. That logger forwards the message across a channel to a waiting parent-playpen-bound fiber, where it just enters the rotation of other fibers getting run. Services don't need to be modified at all. We don't need to have dangling fibers. And for smaller, more contained cases, eh, Go has shown that "just pass the channel around and move on with life" can be an effective approach. The only caveat is you can't pass a channel-based logger to a scope that will be called outside of an async playpen... But that would be the case anyway, so it's not really an issue. There's still the context question, as well as whether spawn is a method on a context object or a keyword, but I think this gets us to 80% of what the original RFC tries to provide, with 20% of the mental overhead. --Larry Garfield

Edmond Dantes

1 year ago
> > This is incorrect. "Create an async bounded context playpen" (what I
called "async" in my example)
> and "start a fiber/thread/task" (what I called "spawn") are two
*separate* operations, and > must remain so.
> >
So, you use *async* to denote the context and *spawn* to create a coroutine. Regarding the context, it seems there's some confusion with this term. Let's try to separate it somehow. 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*.
> > This is probably not the ideal way to structure it in practice, but it
should get the point across.
>
Sounds like a perfect solution. However, the initialization order raises some doubts: it seems that all required coroutines must be created in advance. Will this be convenient? What if a service doesn’t want to initialize a coroutine immediately? What if it’s not loaded into memory right away? *Lazy load.* For example, we have a *Logger* service, which usually starts a coroutine for log flushing. Or even multiple coroutines (e.g., a timer as well). But the service itself might not be initialized and could start only on first use. Should we forbid this practice? If you want to be a service, should you *always* initialize yourself upfront? Wait a minute. This resembles how an OS works. At *level 0*, the operating system runs, while user-level code interacts with it via *interrupts*. It's almost the same as *opening a channel in the ROOT context* and sending a message through the channel from some *child context*. Instead of sending a message directly to the Logger, we could send it to the *service manager* through a channel. Since the *channel was opened in the ROOT context*, all operations would also execute in the *ROOT context*. And if the LOGGER was not initialized, it would be initialized *from the ROOT context*. Possible drawbacks: 1. It's unclear how complex this would be to implement. 2. If messages are sent via a channel, the logger *won't be able to fetch additional data from the request environment*. All data must be explicitly passed, or the *entire context* must be thrown into the channel. Needs more thought. But in any case, the idea with the channel is good. It can cover many scenarios. Everything else is correct, I don’t have much to add. --- Ed.

Rowan Tommins [IMSoP]

1 year ago
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]

Edmond Dantes

1 year ago
> > I think the same thing applies to scheduling coroutines: we want the
Scheduler to take over the "null fiber",
>
Yes, you have quite accurately described a possible implementation. When a programmer loads the initial index.php, its code is already running inside a coroutine. We can call it the main coroutine or the root coroutine. When the index.php script reaches its last instruction, the coroutine finishes, execution is handed over to the Scheduler, and then everything proceeds as usual. Accordingly, if the Scheduler has more coroutines in the queue, reaching the last line of index.php does not mean the script terminates. Instead, it continues executing the queue until... there is nothing left to execute.
> > At that point, the relationship to a block syntax perhaps becomes clearer: >
Thanks to the extensive discussion, I realized that the implementation with startScheduler raises too many questions, and it's better to sacrifice a bit of backward compatibility for the sake of language elegance. After all, Fiber is unlikely to be used by ordinary programmers.

Iliya Miroslavov Iliev

1 year ago
Edmond, The language barrier is bigger (because of me, I cannot properly explain it) so I will keep it simple. Having "await" makes it sync, not async. In hardware we use interrupts but we have to do it grandma style... The main loop checks from variables set on the interrupts which is async. So you have a main loop that checks a variable but that variable is set from another part of the processor cycle that has nothing to do with the main loop (it is not fire and forget style it is in real time). Basically you can have a standard `int main()`function that is sync because you can delay in it (yep sleep(0)) and while you block it you have an event that interrupts a function that works on another register which is independent from the main function. More details of this will be probably not interesting so I will stop. If you want to make async PHP with multiple processes you have to check variables semaphored to make it work. On Sun, Mar 9, 2025 at 8:16 PM Edmond Dantes <edmond.ht@gmail.com> wrote:
> > > > I think the same thing applies to scheduling coroutines: we want the > Scheduler to take over the "null fiber", > > > > Yes, you have quite accurately described a possible implementation. > When a programmer loads the initial index.php, its code is already > running inside a coroutine. > We can call it the main coroutine or the root coroutine. > > When the index.php script reaches its last instruction, the coroutine > finishes, execution is handed over to the Scheduler, and then everything > proceeds as usual. > > Accordingly, if the Scheduler has more coroutines in the queue, reaching > the last line of index.php does not mean the script terminates. Instead, > it continues executing the queue until... there is nothing left to execute. > > > > > At that point, the relationship to a block syntax perhaps becomes > clearer: > > > > Thanks to the extensive discussion, I realized that the implementation > with startScheduler raises too many questions, and it's better to > sacrifice a bit of backward compatibility for the sake of language elegance. > > After all, Fiber is unlikely to be used by ordinary programmers. >
-- Iliya Miroslavov Iliev i.miroslavov@gmail.com

Edmond Dantes

1 year ago
> Edmond, >
....
> If you want to make async PHP with multiple processes you have to check > variables semaphored to make it work. > >
Hello, Iliya. Thank you for your feedback. I'm not sure if I fully understood the entire context. But. At the moment, I have no intention of adding multitasking to PHP in the same way it works in Go. Therefore, code will not require synchronization. The current RFC proposes adding only asynchronous execution. That means each thread will have its own event loop, its own memory, and its own coroutines. P.s. I know also Russian and a bit asm. Ed.

Rob Landers

1 year ago
On Sun, Mar 9, 2025, at 14:17, Rowan Tommins [IMSoP] wrote:
> 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
Slightly off-topic, but you may find the following article interesting: https://manybutfinite.com/post/kernel-boot-process/ It's a bit old, but probably still relevant for the most part. At least for x86. — Rob

Larry Garfield

1 year ago
On Sun, Mar 9, 2025, at 8:17 AM, Rowan Tommins [IMSoP] wrote:
> 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.
Support for nested blocks is absolutely mandatory, whatever else we do. If you cannot nest one async block (scheduler instance, coroutine, whatever it is) inside another, then basically no code can do anything async except the top level framework. This function needs to be possible, and work anywhere, regardless of whether there's an "open" async session 5 stack calls up. function par_map(iterable $it, callable $c) { $result = []; async { foreach ($it as $val) { $result[] = $c($val); } } return $result; } However it gets spelled, the above code needs to be supported. --Larry Garfield

Rowan Tommins [IMSoP]

1 year ago
On 10 March 2025 03:55:21 GMT, Larry Garfield <larry@garfieldtech.com> wrote:
>Support for nested blocks is absolutely mandatory, whatever else we do. If you cannot nest one async block (scheduler instance, coroutine, whatever it is) inside another, then basically no code can do anything async except the top level framework.
To stretch the analogy slightly, this is like saying that no Linux program could call fork() until containers were invented. That's quite obviously not true; in a system without containers, the forked process is tracked by the single global scheduler, and has a default relationship to its parent but also with other top-level processes. Nested blocks are necessary *if* we want automatic resource management around user-selected parts of the program - which is close to being a tautology. If we don't provide them, we just need a defined start and end of the scheduler - and Edmond's current suggestion is that that could be an automatic part of the process / thread lifecycle, and not visible to the user at all.
>This function needs to be possible, and work anywhere, regardless of whether there's an "open" async session 5 stack calls up. > >function par_map(iterable $it, callable $c) { > $result = []; > async { > foreach ($it as $val) { > $result[] = $c($val); > } > } >return $result; >}
This looks to me like an example where you should not be creating an extra context/nursery/whatever. A generic building block like map() should generally not impose resource restrictions on the code it's working with. In fact as written there's no reason for this function to exist at all - if $c returns a Future, a normal array_map will return an array of Futures, and can be composed with await_all, await_any, etc as necessary. If an explicit nursery/context was required in order to use async features, you'd probably want instead to have a version of array_map which took one as an extra parameter, and passed it to along to the callback: function par_map(iterable $it, callable $c, AsyncContext $ctx) { $result = []; async { foreach ($it as $val) { $result[] = $c($val, $ctx); } } return $result; } This is pretty much just coloured functions, but with uglier syntax, since par_map itself isn't doing anything useful with the context, just passing along the one from an outer scope. An awful lot of functions would be like this; maybe FP experts would like it, authors of existing PHP code would absolutely hate it. The place I see nested async{} blocks *potentially* being useful is to have a handful of key "firewalls" in the application, where any accidentally orphaned coroutines can be automatically awaited before declaring a particular task "done". But Daniil is probably right to ask for concrete use cases, and I have not used enough existing async code (in PHP or any other language) to answer that confidently. Rowan Tommins [IMSoP]

Edmond Dantes

1 year ago
>function par_map(iterable $it, callable $c) { > $result = []; > async { > foreach ($it as $val) { > $result[] = $c($val); > } > } >return $result; >}
If the assumption is that each call can be asynchronous and all elements need to be processed, the only proper tool is a concurrent iterator. Manually using a foreach loop is not the best idea because the iterator does not necessarily create a coroutine for each iteration. And, of course, such an iterator should have a getFuture method that allows waiting for the result. Yes, Kotlin has an explicit blocking Scope, but I don’t see much need for it. So far, all the cases we’re considering fit neatly into a framework: 1. I want to launch a coroutine and wait: await spawn 2. I want to launch a coroutine and not wait: spawn 3. I want to launch a group of coroutines and wait: await CoroutineScope 4. I want to launch a group of coroutines and not wait: spawn 5. I want a concurrent iteration: special iterator. What else are we missing?