Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:126687 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by qa.php.net (Postfix) with ESMTPS id C468F1A00BC for ; Mon, 10 Mar 2025 09:19:23 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1741598209; bh=RXIIsNLBDvcbwfekzCwJJpHPmqe5Ctl2MypnMIP8sC0=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=LP3r4NP1RgVIkpNkRZQ1T/X/Xk+nEI2zqL4B9xuntJKYpmqS1ctUDD2nlTLFT2vi0 nJb62x3RvIV6lRyitIMTdT5uKFpnwZ/9wv92VMh9mOdIQ1hawD8ZFdGQ1mDHro63Vz AU3EwriChwPqQzdc/9C4+u16p8GViHoN6xIjTrZqi4RGRUukncrv7G9Zg5hmx3ueDa jydTIyx++GzJPcTzAa66wZTqpH7haC2r1MFJNiRLM0vwfeA6EFB1ZMGkrCW9h87zb/ lggs5jlWZ9sJX244+psTwZPK15S5zURYX6G7zMK1bjAL19piiN3lmfbF3UgqXjdNr5 JvdDeKksH8UzQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id D03D5180068 for ; Mon, 10 Mar 2025 09:16:47 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-3.9 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,FREEMAIL_FROM, HTML_MESSAGE,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE, SPF_PASS autolearn=no autolearn_force=no version=4.0.0 X-Spam-Virus: No X-Envelope-From: Received: from mail-yb1-f173.google.com (mail-yb1-f173.google.com [209.85.219.173]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Mon, 10 Mar 2025 09:16:47 +0000 (UTC) Received: by mail-yb1-f173.google.com with SMTP id 3f1490d57ef6-e549be93d5eso3456476276.1 for ; Mon, 10 Mar 2025 02:19:21 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1741598361; x=1742203161; darn=lists.php.net; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=Y50kYMcwTIpE0MbwgNEb+hixnLLvda5F2wIjloQXUTc=; b=LJPp6X6FPP+iDv0HKH3U55ikmnIJRReeOiWQr1kAAec8AWhDKeVD9jYMfciqf6C5ch Gd3cLV0cORhHkdWfbs3V1EDdyTXO6DsDjaZWB91R8u/s5DXKhqckdgOIsHzjRB3SsWet YPPjE/e3y9wC4qYWZQkDvq/PQbeX31A/fub4Dr/hqg+5yZuWXozOTDYn+XUQGSeiVtqv jFYanQ02OVy575NCxEXVCO1YxYT9aodUOWYsgj0xkWcaOUrUpal+tzQeh01ZeZuD+0Gt meU+PPpDj6SYd6tSePz4psWq/wA7Hy2xMRod7fJV+1q2t24ZCtGumH9D/qsiL210zilg ylyA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1741598361; x=1742203161; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=Y50kYMcwTIpE0MbwgNEb+hixnLLvda5F2wIjloQXUTc=; b=pwxIg5fi7YmC7aO3TByKEPvfAoyS3k4/0L2IDpHtUyxy3NiuNmJ0RLkX9UDzdbsRoY Lfa+wizmSd04HZfMi/D/CQVWm0pzax0tDOiG8+6h/N/MtDs0+klkHWkq82ayYWHEURm2 NNZEJyeFZfr7Og9V1cIZ48tWHCT1tCzAlS7RCIjW9LH2efri5aHnE0FAIc6YLafZ6Vo8 SV7eBfUUW290riErGkElkfTCTbRWkscHDIjtqd9kcqaNb12VnNQSegx8P9pJ5mNrJsXx nrINpMe1FF34+UUJ2TDiDnfsGKPl/ak81lM+hDBgobWPHt9JIJylHiPiBd7UUZxg6+eb Mbkg== X-Gm-Message-State: AOJu0YzTjGek/dn5cA+YcqLZJyRB/qna7k5cZ8idmG0GAGd68U64p3sp MCIXBoV9JNZMz7whWAmrbAvo0lqn7O7SOiqFMNDFHQV8mjTm0nxdjWwiqSyySKzpDALc6TqR35S j9RQN+F9Glq6jp6jX2HKhYpQshYohO5ivBrk= X-Gm-Gg: ASbGncumW5Hv4FAYwSZ+ymuzU6ZPIGWznaW7GyqZl1TIucm2mF0kbCNIXCOfvixA+D6 duFqCHQMbmqhgn1NfE1+NV98ymxqRBkxueCt+Hg/IER5G1ZqY4k0NWWfC1PWV44s22WEqFUYvO/ 2tkBo4yYAHRWtkV4Yrng2ZcRpu0Q== X-Google-Smtp-Source: AGHT+IFQYUrapwfbQ9KOwfbH/6ZxWUrEZyn3It34CPpssVGAmHE43sA4aV2OWOl0HNayjym8Vzlr5tcnqr2e2Q+oRSI= X-Received: by 2002:a05:6902:2291:b0:e5b:12b4:d67 with SMTP id 3f1490d57ef6-e635c136520mr16458641276.11.1741598361182; Mon, 10 Mar 2025 02:19:21 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 References: <08c8ad0b-e8f4-46e3-99f0-b80748d40b89@app.fastmail.com> <07973EAE-2D83-47A8-8FA0-84286C77C02B@rwec.co.uk> <48d66433-3ae9-4895-8361-7c81a0a3670d@app.fastmail.com> <8599eb8b-d4a3-4cb8-899a-25b134e0d64d@gmail.com> <74c4c726-63aa-44e0-84c9-840e13a65a4f@gmail.com> <77DC5F50-531D-49E8-8BE2-504A19CB5FFD@rwec.co.uk> <676e36e4-0b84-4d8c-b3db-2998831cd79d@gmail.com> <510B8EF1-9D07-41A8-9EA0-7D99CF7BFC91@rwec.co.uk> <4690cff0-7a48-4fe0-8310-688be253f976@gmail.com> <4871b2e4-353e-424f-92b9-7e2bb753bafa@gmail.com> <2b7dfacf-fd9f-4692-8b92-b2f22591d32a@app.fastmail.com> In-Reply-To: <2b7dfacf-fd9f-4692-8b92-b2f22591d32a@app.fastmail.com> Date: Mon, 10 Mar 2025 11:19:10 +0200 X-Gm-Features: AQ5f1JokDWvazP9RmWx0WHkIl74FuFTwXwzlI3L428kiXMQiRGksYPSLbeuHXwo Message-ID: Subject: Re: [PHP-DEV] PHP True Async RFC To: Larry Garfield Cc: php internals Content-Type: multipart/alternative; boundary="000000000000a8e88b062ff9777d" From: edmond.ht@gmail.com (Edmond Dantes) --000000000000a8e88b062ff9777d Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable > > 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-concurr= ency 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 =3D 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 =3D waitForIncomingConnection(); // Creating a new Scope for handling the request $requestScope =3D 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 =3D 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=E2=80=99t 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 *logic= ally 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=E2=80=99s p= arent*? 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 *=E2=80= =94 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 *co= de as an argument* against *purely logical reasoning*. --- Ed --000000000000a8e88b062ff9777d Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
>
>=C2=A0 As noted, I am in broad agreement with the previously linked article on &qu= ot;playpens" (even if I hate that name), that the "go style model= " is too analogous to goto statements.
>
The syntax and logi= c you describe are very close to Kotlin's implementation.

I would say that Kotlin is probably the best example of structured concurre= ncy 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 relati= onships, Kotlin binds them to execution contexts. At the same time, the GlobalScope context is accessible everywhere.

https://kotlinlang.org/docs/coroutines-and-channels.html#structur= ed-concurrency

So, it is not the coroutine that maintains the hie= rarchy, but the Scope object, which essentially aligns wit= h 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 corouti= ne that should wait, but the Scope that should "wait.= "

```php

spawn function {
=C2=A0 =C2=A0 echo "c1\n= ";
=C2=A0 =C2=A0 spawn function {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 ec= ho "c2\n";
=C2=A0 =C2=A0 };

=C2=A0 =C2=A0 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 ass= ociated coroutines are completed. This memory model is entirely fair.=C2=A0= =C2=A0

Let's consider a scenario with libraries. A library may wa= nt to run coroutines under its own control. This means that the library wan= ts to execute coroutines within its own Scope. For example= :=C2=A0=C2=A0

```php

class Logger {
=C2=A0 =C2=A0public func= tion __construct() {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0$this->scope = =3D new CoroutineScope();
=C2=A0 =C2=A0 }

=C2=A0 =C2=A0public fun= ction log(mixed $data) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0// Adding ano= ther coroutine to our personal Scope
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0$= this->scope->spawn($this->handle_log(...), $data);
=C2=A0 =C2= =A0}

=C2=A0 =C2=A0public function __destruct()
=C2=A0 =C2=A0{
= =C2=A0 =C2=A0 =C2=A0 // We can explicitly cancel all coroutines in the dest= ructor if we find it appropriate
=C2=A0 =C2=A0 =C2=A0 $this->scope->cancel();
=C2=A0 =C2=A0}}

```

Default Behavior

By default, the context is = always inherited when calling spawn, so there is no need to pa= ss it explicitly. The expression spawn function {} is essentia= lly equivalent to currentScope->spawn.

The behavior of an HTTP server in a long-running process would look = like this:

```php

function receiveLoop()
{
=C2=A0 =C2=A0 = while (true) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 // Simulating waiting for an = incoming connection
=C2=A0 =C2=A0 =C2=A0 =C2=A0 $connection =3D waitForI= ncomingConnection();

=C2=A0 =C2=A0 =C2=A0 =C2=A0 // Creating a new = Scope for handling the request
=C2=A0 =C2=A0 =C2=A0 =C2=A0 $requestScope= =3D new CoroutineScope();

=C2=A0 =C2=A0 =C2=A0 =C2=A0 // Processing= the request inside its own scope
=C2=A0 =C2=A0 =C2=A0 =C2=A0 $requestSc= ope->spawn(function () use ($connection) {
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 handleRequest($connection);
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 });
=C2=A0 =C2=A0 }
}

```

Scope allows the use of= the "task group" and "await all&qu= ot; patterns without additional syntax, making it convenient.=C2= =A0=C2=A0

```php
$scope =3D new CoroutineScope();

$scope-&g= t;spawn(function () {
=C2=A0 =C2=A0 echo "Task 1 started\n";=C2=A0 =C2=A0 sleep(1);
=C2=A0 =C2=A0 echo "Task 1 finished\n&quo= t;;
});

$scope->spawn(function () {
=C2=A0 =C2=A0 echo &quo= t;Task 2 started\n";
=C2=A0 =C2=A0 sleep(2);
=C2=A0 =C2=A0 echo = "Task 2 finished\n";
});

// Wait for all tasks to compl= ete
$scope->awaitAll();

```

What is the advantage of= using Scope instead of parent-child relationships in coro= utines?

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 langua= ges, especially those designed for business logic.

= Who Will Use Coroutines?

I believe that the primary= consumers of coroutines are libraries and frameworks, whi= ch will provide developers with services and logic to solve tasks.

If= a library refuses to consider that its coroutine may be <= strong>canceled by the user's code, or how it should be cancel= ed, it means the library is neglecting its responsibility = to provide a proper contract.

The default co= ntract must give the user the power to terminate all coroutines launched within a given co= ntext because only the user of the library knows when this= needs to be done. If a library has a different opinion, t= hen it is obligated to explicitly implement this behavior.=

This means that libraries and services bear greater responsi= bility than their users. But isn=E2=80=99t that already the case?<= /p>

>
>=C2=A0The language simply will not let you write=C2=A0memory-unsafe code
>

= And this is more of an anti-example than an example.=C2=A0= =C2=A0

But this analogy, like any other, cannot be used as an argumen= t for or against. Memory safety is not the same as launching a coroutine in GlobalScope.

Where is the danger here? That the coroutine does somethin= g? But why is that inherently bad?

It only becomes a problem= when a coroutine accidentally captures memory from the cu= rrent 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 possib= ility.

If a coroutine does not capture incorrect objects a= nd does not wait on the wrong $channel, then = it should not be considered an issue.

>
>= =C2=A0I'm not sure if Corou= tine would be the right name either
>

I'm no= t an expert in choosing good names, so I rely on others' opinions. If t= he term coroutine feels overused, we can look for somethin= g else. But what?

> ($inner->parent ?? $inner)->spawn(escape(..=
.));

But the meaning of this code raises a question: why am I p= lacing my coroutine in the parent's context if my own context is alread= y 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=E2=80=99s parent?

It makes an assumption about something it cannot possibly kn= ow.

I would suggest explicitly specifying which Scop= e we want:=C2=A0=C2=A0

```php

function stuff() {
= =C2=A0 =C2=A0 async $inner {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 // While the re= quest is active
=C2=A0 =C2=A0 =C2=A0 =C2=A0 ($inner->find('reques= tScope') ?? $inner)->spawn(escape(...));

=C2=A0 =C2=A0 =C2=A0= =C2=A0 // Or while the server is running
=C2=A0 =C2=A0 =C2=A0 =C2=A0 ($= inner->find('serverScope') ?? $inner)->spawn(escape(...));=C2=A0 =C2=A0 }
}

```

>
> Edmund, does that ma= ke any sense to you?
>=C2=A0=C2=A0

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 reasonabl= e=C2=A0=E2=80=94 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, whic= h will focus on a much narrower topic. So 100% yes.

In addition to expert input, I would also like to create=C2= =A0a database of real-world use cases from code and examples. This= would allow us to use code as an argument against purely logical reasoning.=C2=A0

---

Ed

--000000000000a8e88b062ff9777d--