Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:129236 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 lists.php.net (Postfix) with ESMTPS id 902751A00BC for ; Sat, 15 Nov 2025 19:56:21 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1763236586; bh=BfANgW3jT1kdjSkyDJLYi2BQRJex84PlkdXzVIU3IjI=; h=Date:From:To:Cc:In-Reply-To:References:Subject:From; b=CodTFsjd1GW5cyKCX0dAjNzh4jzckWfieh4dJ2I45Uaca5xfOUlTzgGnuHwEXrKpw 7wDEAtat+lx0OTdBqk2Vl9X4a/q9F5ISR1e52ApPUEmx5Hp/RcWFbTTP9sBSDalNn7 37a1lS07Ue++ywIcw+7cZTK48IcK8sAzZdtvvC2JLRq8MaSaFJMgXp97PVlXvKoVGh mDINLTaSNjNFqMhb9pu4w2vHXgojGgmuxcxUTy8pZStsPTZ5kUw3olMcfiriubgUXN aqtXAnlAmZAK7uoPETnFJjKdjto7C+mBROYjRMWPEmlOQet/NnSDPwDn/lmu/PBhqh KRWDv2gEXQusw== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 9F914180081 for ; Sat, 15 Nov 2025 19:56:24 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-0.1 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,HTML_MESSAGE, RCVD_IN_DNSWL_LOW,SPF_HELO_PASS,SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from fout-b1-smtp.messagingengine.com (fout-b1-smtp.messagingengine.com [202.12.124.144]) (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 ; Sat, 15 Nov 2025 19:56:24 +0000 (UTC) Received: from phl-compute-12.internal (phl-compute-12.internal [10.202.2.52]) by mailfout.stl.internal (Postfix) with ESMTP id D07B91D0010A; Sat, 15 Nov 2025 14:56:18 -0500 (EST) Received: from phl-imap-05 ([10.202.2.95]) by phl-compute-12.internal (MEProxy); Sat, 15 Nov 2025 14:56:18 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bottled.codes; h=cc:cc:content-type:content-type:date:date:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to; s=fm1; t=1763236578; x= 1763322978; bh=xjZTw+0V+jz5M6Edbf6oMtYBqEdoO0V87s8PrlCXtOY=; b=r KUGLbsNmGCpkTE6qOtSEXQF+qq4oD13Hxd97O41t2mYKGP15hLjRU2ewiyZ7czjo 3R99X6PyhvVjZkyya7JBvKkHbgzSFcvefYD2s3d/SmHP46ERiymP+sQlLcOuiV15 93IvEro6/ECF+CL/w5CauIXXb50sNq+SEzYtRKOUFw2Z8FzNtB8Qmvvxd9HHMsIH xzfj7ASvaSWxR9sRjbLAzkAqkneDtQlGa2KEa2TUvMhpC1oWybZleutnHRfbtt7L VBuHJhoVMT6sbKnTcYcSXF9EaKAzXfXunhHFrWgGl3HVYKtAsHHSWCH16REFHR4P qd+HDyuUnm0gosFQNoE8Q== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t= 1763236578; x=1763322978; bh=xjZTw+0V+jz5M6Edbf6oMtYBqEdoO0V87s8 PrlCXtOY=; b=GtaafhUDJzY63Dl1XKnmb/7qI4OBklY/wNOXIcOwQK3+3kDjk6K hqS1lfCgszOQIV5Xv6X23HNbiYzbT39I4vsDw/mcnPISg6YhJI3LG6RbVrzboz+8 ChlUmz8Sg161fegi9BoDsZSczKi0B5pSuX98MeRnjEedwMOnp48RhMAH8naeod+e 1ZwiGpsKVneXyodb3O+8t5Co6RmswGMLEPralLQzcYaCDDGijo6FBQ55dV7P6msZ qVtpN8+uvaGuxair50r6Qo89GD2nKeN/ANFQ4LrLomN5fXqVuIbpLmVSk70aEhad HmxRDM6bkT45ZMHufEMkixTZV60JcD1f8WQ== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggddvudefheelucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujf gurhepofggfffhvfevkfgjfhfutgesrgdtreerredtjeenucfhrhhomhepfdftohgsucfn rghnuggvrhhsfdcuoehrohgssegsohhtthhlvggurdgtohguvghsqeenucggtffrrghtth gvrhhnpeetteejveeihfdvhefhheeffeeugfffieektefhfeeuvddtgfehjeevgfejjeel keenucffohhmrghinhephhhtthhpuhhrlhdrrghnpdhgohdruggvvhenucevlhhushhtvg hrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehrohgssegsohhtthhlvggu rdgtohguvghspdhnsggprhgtphhtthhopeegpdhmohguvgepshhmthhpohhuthdprhgtph htthhopehlrghrrhihsehgrghrfhhivghlughtvggthhdrtghomhdprhgtphhtthhopegv ughmohhnugdrhhhtsehgmhgrihhlrdgtohhmpdhrtghpthhtohepihhnthgvrhhnrghlsh eslhhishhtshdrphhhphdrnhgvthdprhgtphhtthhopegsuhhkkhgrsehphhhprdhnvght X-ME-Proxy: Feedback-ID: ifab94697:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id 668A81820074; Sat, 15 Nov 2025 14:56:17 -0500 (EST) X-Mailer: MessagingEngine.com Webmail Interface Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow MIME-Version: 1.0 X-ThreadId: A2k3WxiDp57Z Date: Sat, 15 Nov 2025 20:55:56 +0100 To: "Edmond Dantes" Cc: "php internals" , "Jakub Zelenka" , "Larry Garfield" Message-ID: In-Reply-To: References: <6618a91c-5393-4f40-88b5-b5041ee09deb@app.fastmail.com> <3e0cf0a1-c1a3-4e05-97ba-0eeb7f559a53@app.fastmail.com> Subject: Re: [PHP-DEV] Re: PHP True Async RFC Stage 5 Content-Type: multipart/alternative; boundary=732d1444f6ff4c2daff3057e415df6e7 From: rob@bottled.codes ("Rob Landers") --732d1444f6ff4c2daff3057e415df6e7 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable On Sat, Nov 15, 2025, at 15:41, Edmond Dantes wrote: > Hello. >=20 > > Based on the conversation so far, I=E2=80=99d imagine the list to lo= ok something like: >=20 > Yes, that=E2=80=99s absolutely correct. When a programmer uses an oper= ation > that would normally block the entire thread, control is handed over to > the Scheduler instead. > The suspend function is called inside all of these operations. I think that "normally" is doing a lot of work here. `fwrite()` can bloc= k, but often doesn=E2=80=99t. `file_get_contents()` is usually instant f= or local files but can take seconds on NFS or with an HTTP URL. An `arra= y_map()` *always* blocks the thread but should *never* suspend. Without very clear rules, it becomes impossible to reason about what=E2=80= =99ll suspend and what won=E2=80=99t. >=20 > > If that=E2=80=99s the intended model, it=E2=80=99d help to have that= spelled out directly; it makes it immediately clear which functions can= or will suspend and prevents surprises. >=20 > In the Async implementation, it will be specified which functions are = supported. This is exactly the kind of thing that needs to be in the RFC itself. Re= lying on "the implementation will document it" creates an unstable contr= act. Even something simple like: - if it can perform network IO - if it can perform file/stream IO - if it can sleep or wait on timers - if it awaits a `FutureLike` - if it calls `suspend()` This would then create a stable baseline and require an RFC to change th= e rules, forcing people to think through BC breakages and ecosystem impa= ct. >=20 > > I also think the RFC needs at least minimal wording about scheduler = guarantees, even if the details are implementation-specific. > The Scheduler guarantees that a coroutine will be invoked if it is in = the queue. That=E2=80=99s not quite enough. The order really matters. Different sch= edulers produce different observable results. For example: function step(string $name, string $msg) { echo "$name: $msg\n"; suspend(); } spawn(function() { step("A", "1"); step("A", "2"); step("A", "3"); }); spawn(function() { step("B", "1"); step("B", "2"); step("B", "3"); }); spawn(function() { step("C", "1"); step("C", "2"); step("C", "3"); }); Under different scheduling strategies you get different, but stable patt= erns. Consider FIFO or round-robin, run-to-suspend: A: 1 B: 1 C: 1 A: 2 B: 2 Cl: 2 A: 3 B: 3 C: 3 But with a stack-like or LIFO strategy, running-to-suspend: A: 1 B: 1 C: 1 C: 2 C: 3 B: 2 B: 3 A: 2 A: 3 Both are valid, but are important to *know* which one is implemented, an= d if someone wants to replace the scheduler, they also need to ensure th= ey guarantee this behaviour. >=20 > > For example, is the scheduler run-to-suspend? FIFO or round-robin w= akeup? And non-preemptive behaviour only appears here in the thread. It = isn=E2=80=99t mentioned in the RFC itself. >=20 > In Go, for example, when it was still cooperative, these details were > also not part of any public contract. The only guarantee Go provided > was that a coroutine would not be interrupted arbitrarily. The same > applies to this RFC: coroutines are interrupted only at designated > suspension points. > However, neither Go nor any other language exposes the internal > details of the Scheduler as part of a public contract, because those > details may change without notice. Go did document these details during its cooperative era, including exac= tly where goroutines might yield. Unfortunately, I can=E2=80=99t find a = link to documentation that old. I did come across the old design docs th= at might shed some light on how things worked back then: https://go.dev/= wiki/DesignDocuments The key point is that Go made cooperative scheduling predictable enough = that developers could write performant code without guessing. >=20 > > That=E2=80=99s important for people writing long, CPU-bound loops, s= ince nothing will interrupt them unless they explicitly yield. > Hypothetically, in the future it may become possible to interrupt > loops, just like Go eventually did. This would likely require an > additional RFC. PHP does have the ability to interrupt a loop at any > point, but most likely only for terminating execution. > This RFC does nothing of the sort. My concern isn=E2=80=99t the lack of loop preemption. My concern is that= the RFC never *says* CPU loops *don=E2=80=99t yield.* If it isn=E2=80=99= t stated explicitly, it won=E2=80=99t be documented, and users will disc= over it the hard way. That=E2=80=99s exactly the sort of footgun we shou= ld avoid at the language level. > > Lastly, cancellation during a syscall is still unclear. If a corouti= ne is cancelled while something like fwrite() or a DB write is in progre= ss, what should happen? > > Does fwrite() still return the number of bytes written? Does it thro= w? For write-operations in particular, this affects whether applications= can maintain a consistent state. >=20 > If the write operation is interrupted, the function will return an > error according to its contract. In this case, it will return false. `fwrite()` almost never returns `false`, it returns "bytes written OR fa= lse". Partial successful writes are normal and extremely common. So, can= cellation *does* change the behaviour unless this is spelled out very ca= refully so calling code can recover appropriately. >=20 > > Clarifying these points would really help people understand how to r= eason about concurrency with this API. >=20 > This is described in the document. I may be missing something, but I don=E2=80=99t see this spelled out any= where in the RFC. > There is, of course, a nuance regarding extended error descriptions, > but at the moment no such changes are planned. That=E2=80=99s fine, but then do you expect the RFC to pass as-is? Right= now, without suspension rules, scheduler guarantees, defined syscall-ca= ncellation semantics, it=E2=80=99s tough to evaluate the correctness and= performance implications. Leaving some of the most important aspects as= an "implementation detail" seems like asking for trouble. =E2=80=94 Rob --732d1444f6ff4c2daff3057e415df6e7 Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable
On Sat, Nov = 15, 2025, at 15:41, Edmond Dantes wrote:
Hello.

> Based on = the conversation so far, I=E2=80=99d imagine the list to look something = like:

Yes, that=E2=80=99s absolutely correct. W= hen a programmer uses an operation
that would normally block t= he entire thread, control is handed over to
the Scheduler inst= ead.
The suspend function is called inside all of these operat= ions.

I think that "normally" is d= oing a lot of work here. fwrite() can block, but often doesn=E2=80=99t. file_get_contents() is usua= lly instant for local files but can take seconds on NFS or with an HTTP = URL. An array_map()<= /code> always blocks the thread but should never suspend.<= /div>

Without very clear rules, it becomes impossible= to reason about what=E2=80=99ll suspend and what won=E2=80=99t.


> If that=E2=80=99s the intended model, it=E2=80=99d help to = have that spelled out directly; it makes it immediately clear which func= tions can or will suspend and prevents surprises.

In the Async implementation, it will be specified which functions are= supported.

This is exactly the ki= nd of thing that needs to be in the RFC itself. Relying on "the implemen= tation will document it" creates an unstable contract.

Even something simple like:

- if it can= perform network IO
- if it can perform file/stream IO
- if it can sleep or wait on timers
- if it awaits a FutureLike
- if it calls susp= end()

This would then create a stable ba= seline and require an RFC to change the rules, forcing people to think t= hrough BC breakages and ecosystem impact.


> I also thi= nk the RFC needs at least minimal wording about scheduler guarantees, ev= en if the details are implementation-specific.
The Scheduler g= uarantees that a coroutine will be invoked if it is in the queue.
<= /blockquote>

That=E2=80=99s not quite enough. The ord= er really matters. Different schedulers produce different observable res= ults.

For example:

fun= ction step(string $name, string $msg) {
  echo "$name: $m= sg\n";
  suspend();
}

= spawn(function() { step("A", "1"); step("A", "2"); step("A", "3"); });
spawn(function() { step("B", "1"); step("B", "2"); step("B", "3= "); });
spawn(function() { step("C", "1"); step("C", "2"); ste= p("C", "3"); });

Under different scheduling str= ategies you get different, but stable patterns.

Consider FIFO or round-robin, run-to-suspend:

= A: 1
B: 1
C: 1
A: 2
B: 2
Cl: 2
A: 3
B: 3
C: 3

But with a stack-like or LIFO strategy, running-to-suspend:
=

A: 1
B: 1
C: 1
C: 2
C: 3
B: 2
B: 3
A: 2
A: 3=

Both are valid, but are important to know which one is implemented, and if someone wants to replace the schedul= er, they also need to ensure they guarantee this behaviour.


>  For example, is the scheduler run-to-suspend? FIFO or = round-robin wakeup? And non-preemptive behaviour only appears here in th= e thread. It isn=E2=80=99t mentioned in the RFC itself.

In Go, for example, when it was still cooperative, these detail= s were
also not part of any public contract. The only guarante= e Go provided
was that a coroutine would not be interrupted ar= bitrarily. The same
applies to this RFC: coroutines are interr= upted only at designated
suspension points.
However,= neither Go nor any other language exposes the internal
detail= s of the Scheduler as part of a public contract, because those
details may change without notice.

Go did document these details during its cooperative era, including ex= actly where goroutines might yield. Unfortunately, I can=E2=80=99t find = a link to documentation that old. I did come across the old design docs = that might shed some light on how things worked back then: https://go.dev/wiki/DesignDocument= s

The key point is that Go made cooperative= scheduling predictable enough that developers could write performant co= de without guessing.


> That=E2=80=99s important for pe= ople writing long, CPU-bound loops, since nothing will interrupt them un= less they explicitly yield.
Hypothetically, in the future it m= ay become possible to interrupt
loops, just like Go eventually= did. This would likely require an
additional RFC. PHP does ha= ve the ability to interrupt a loop at any
point, but most like= ly only for terminating execution.
This RFC does nothing of th= e sort.

My concern isn=E2=80=99t t= he lack of loop preemption. My concern is that the RFC never say= s CPU loops don=E2=80=99t yield. If it isn=E2=80=99t sta= ted explicitly, it won=E2=80=99t be documented, and users will discover = it the hard way. That=E2=80=99s exactly the sort of footgun we should av= oid at the language level.

> Lastly, cancellation during a syscall is= still unclear. If a coroutine is cancelled while something like fwrite(= ) or a DB write is in progress, what should happen?
> Does = fwrite() still return the number of bytes written? Does it throw? For wr= ite-operations in particular, this affects whether applications can main= tain a consistent state.

If the write operation= is interrupted, the function will return an
error according t= o its contract. In this case, it will return false.

fw= rite() almost never returns false, it returns "bytes written OR false". P= artial successful writes are normal and extremely common. So, cancellati= on does change the behaviour unless this is spelled out very care= fully so calling code can recover appropriately.


> Cla= rifying these points would really help people understand how to reason a= bout concurrency with this API.

This is describ= ed in the document.

I may be missi= ng something, but I don=E2=80=99t see this spelled out anywhere in the R= FC.

<= div>There is, of course, a nuance regarding extended error descriptions,=
but at the moment no such changes are planned.

That=E2=80=99s fine, but then do you expect the = RFC to pass as-is? Right now, without suspension rules, scheduler guaran= tees, defined syscall-cancellation semantics, it=E2=80=99s tough to eval= uate the correctness and performance implications. Leaving some of the m= ost important aspects as an "implementation detail" seems like asking fo= r trouble.

=E2=80=94 Ro= b
--732d1444f6ff4c2daff3057e415df6e7--