>
> As noted, I am in broad agreement with the previously linked article on
"playpens" (even if I hate that name), that the "go style model" is too
analogous to goto statements.
>
The syntax and logic you describe are very close to Kotlin's implementation..
I would say that Kotlin is probably the best example of structured
concurrency organization, which is closest to PHP in terms of abstraction
level.
One downside I see in Kotlin's syntax is its complexity.
However, what stands out is the *CoroutineScope* concept. Instead of
linking coroutines through *Parent-Child* relationships, Kotlin binds them
to execution contexts. At the same time, the *GlobalScope* context is
accessible everywhere.
https://kotlinlang.org/docs/coroutines-and-channels.html#structured-concurrency
So, it is not the coroutine that maintains the hierarchy, but the *Scope*
object, which essentially aligns with the RFC proposal: contexts can be
hierarchically linked.
This model sounds promising because it relieves coroutines from the
responsibility of waiting for their child coroutines. It is not the
coroutine that should wait, but the *Scope* that should "wait."
```php
spawn function {
echo "c1\n";
spawn function {
echo "c2\n";
};
echo "c1 end\n";
};
```
There is no reason to keep *coroutine1* in memory if it does not need
*coroutine2*. However, the *Scope* will remain in memory until all
associated coroutines are completed. This memory model is entirely fair.
Let's consider a scenario with libraries. A library may want to run
coroutines under its own control. This means that the library wants to
execute coroutines within its own *Scope*. For example:
```php
class Logger {
public function __construct() {
$this->scope = new CoroutineScope();
}
public function log(mixed $data) {
// Adding another coroutine to our personal Scope
$this->scope->spawn($this->handle_log(...), $data);
}
public function __destruct()
{
// We can explicitly cancel all coroutines in the destructor if we
find it appropriate
$this->scope->cancel();
}
}
```
Default Behavior
By default, the context is always inherited when calling spawn, so there is
no need to pass it explicitly. The expression spawn function {} is
essentially equivalent to currentScope->spawn.
The behavior of an HTTP server in a long-running process would look like
this:
```php
function receiveLoop()
{
while (true) {
// Simulating waiting for an incoming connection
$connection = waitForIncomingConnection();
// Creating a new Scope for handling the request
$requestScope = new CoroutineScope();
// Processing the request inside its own scope
$requestScope->spawn(function () use ($connection) {
handleRequest($connection);
});
}
}
```
Scope allows the use of the *"task group"* and *"await all"* patterns
without additional syntax, making it convenient.
```php
$scope = new CoroutineScope();
$scope->spawn(function () {
echo "Task 1 started\n";
sleep(1);
echo "Task 1 finished\n";
});
$scope->spawn(function () {
echo "Task 2 started\n";
sleep(2);
echo "Task 2 finished\n";
});
// Wait for all tasks to complete
$scope->awaitAll();
```
What is the advantage of using *Scope* instead of parent-child
relationships in coroutines?
If a programmer *never* uses *Scope*, then the behavior is literally the
same as in *Go*. This means that the programmer does not need structured
relationships, and it does not matter when a particular coroutine completes..
At the same time, code at a *higher level* can control the execution of
coroutines created at *lower levels*. The known downside of this approach
is that if lower-level code needs its coroutines to run *independently*, it
must explicitly define this behavior.
According to analysis, this model is effective in most modern languages,
especially those designed for *business logic*.
*Who Will Use Coroutines?*
I believe that the primary consumers of coroutines are *libraries and
frameworks*, which will provide developers with services and logic to solve
tasks.
If a library *refuses* to consider that its coroutine may be *canceled* by
the user's code, or how it should be canceled, it means the library is
*neglecting* its responsibility to provide a proper *contract*.
The *default contract* must give the *user* the *power* to *terminate all
coroutines* launched within a given context because *only the user of the
library knows* when this needs to be done. If a library *has a different
opinion*, then it is *obligated to explicitly implement this behavior*.
This means that *libraries and services bear greater responsibility* than
their users. But isn’t that already the case?
>
> The language simply will not let you write memory-unsafe code
>
And this is more of an *anti-example* than an example.
But this analogy, like any other, cannot be used as an argument *for* or
*against*. *Memory safety* is not the same as launching a coroutine in
GlobalScope.
Where is the danger here? That the coroutine *does something*? But why is
that inherently bad?
It only becomes a problem when a coroutine *accidentally captures memory*
from the current context via use(), leaving an object in memory that *logically
should have been destroyed* when the request *Scope* was destroyed.
However, you *cannot force* a programmer to avoid writing such code.
Neither *nurseries* nor *structured concurrency* will take away this
possibility.
If a coroutine *does not capture incorrect objects* and *does not wait on
the wrong $channel*, then it should *not* be considered an issue.
>
> I'm not sure if Coroutine would be the right name either
>
I'm not an expert in choosing good names, so I rely on others' opinions. If
the term *coroutine* feels overused, we can look for something else. But
what?
> ($inner->parent ?? $inner)->spawn(escape(...));
But the meaning of this code raises a question: *why am I placing my
coroutine in the parent's context if my own context is already inherited
from the parent?*
Or do I want my coroutine to be destroyed *only* with the parent's context,
but *not* with the current one? But then, *how do I know* that I should
place the coroutine in the *parent*, rather than in the *parent’s parent*?
It makes an assumption about something it *cannot possibly know*.
I would suggest explicitly specifying which *Scope* we want:
```php
function stuff() {
async $inner {
// While the request is active
($inner->find('requestScope') ?? $inner)->spawn(escape(...));
// Or while the server is running
($inner->find('serverScope') ?? $inner)->spawn(escape(...));
}
}
```
>
> Edmund, does that make any sense to you?
>
If there are *expert-level* people who have spent years working on language
syntax while also having a *deep understanding of asynchrony*, and they are
willing to help us, I would say that this is not just *reasonable *— it is
more like a *necessary step* that absolutely must be taken.
However, *not for the current RFC*, but rather for a *draft of the next one*,
which will focus on a much *narrower* topic. So 100% yes.
In addition to expert input, I would also like to create a database of
real-world use cases from code and examples. This would allow us to use *code
as an argument* against *purely logical reasoning*.
---
Ed