Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:128908 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 B2DB21A00C5 for ; Wed, 22 Oct 2025 17:24:43 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1761153888; bh=C2Cy1D5Ep+ouDxEpkzgfOrgI4R37Y0i176Epf/z+d8I=; h=Subject:From:In-Reply-To:Date:Cc:References:To:From; b=CdQWeRHszBxK4Srs1AEaLFKzbJIXCS5XqX6c7ck65/voda0FsfSS4UKrRx8GzeTfZ Sr2yMqbMebT0voW7TuYknU4f8ROmEklPfraXHm8bg41EpZfLU6cF/PKuAWexbBijld Vbs0U4lioUoNRbGF2ZHtEnMX6aCB/nvxTQH74Q+XEWorj178rK5HtYk0x4Y2RiExVB U1ODGpFQuB8f81sbbi+AooAFA5nBjks827vyDLtITf9KNLkn6le9K2nLqsQnnj8pmX FJlP0rhLSHf+3xavhHseDEZUMd7gPi0qZDIRPhjeCIuWK1epIQoXqpm1sTq77oCEhT aILFY0A8J3X1A== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 693A11801DB for ; Wed, 22 Oct 2025 17:24:47 +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.6 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,SPF_HELO_NONE, SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: No X-Envelope-From: Received: from nebula.zort.net (nebula.zort.net [96.241.205.3]) (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 ; Wed, 22 Oct 2025 17:24:47 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=zort.net; s=zort; t=1761153881; bh=C2Cy1D5Ep+ouDxEpkzgfOrgI4R37Y0i176Epf/z+d8I=; h=Subject:From:In-Reply-To:Date:Cc:References:To:From; b=WQdG1k9LAePfBq1WLniYPx65cs5gMD/yttw4UeRtPVOUzO2799gt6RQmAwbO1pieU 0kQkLxyyaTfzHCYBnAC+wIhcbesqCqFGdSDLQmUAhVwzgjlvpbFZZECCT/ncq7Su0Q +qLtrd6GnKwdM+mmUfQd/RzZvcdbIkr009Ys+0Ew= Received: from smtpclient.apple (pulsar.zort.net [96.241.205.6]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by nebula.zort.net (Postfix) with ESMTPSA id A0BC91300503; Wed, 22 Oct 2025 13:24:41 -0400 (EDT) Content-Type: text/plain; charset=us-ascii Precedence: list list-help: list-unsubscribe: list-post: List-Id: x-ms-reactions: disallow Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3826.700.81\)) Subject: Re: [PHP-DEV] PHP True Async RFC Stage 4 In-Reply-To: Date: Wed, 22 Oct 2025 13:24:31 -0400 Cc: Rob Landers , Aleksander Machniak , PHP Internals Content-Transfer-Encoding: quoted-printable Message-ID: References: <0e4e39d6-9cc9-4970-92e0-2463143b4011@app.fastmail.com> <37180d8d-85b4-49a3-a672-334bf4329470@app.fastmail.com> <2f8524a7-dea2-4fbf-933a-c538d3706253@app.fastmail.com> <151800a7-1094-49bc-8e43-c593a74741af@app.fastmail.com> To: Edmond Dantes X-Mailer: Apple Mail (2.3826.700.81) From: jbafford@zort.net (John Bafford) Hi Edmond, > On Oct 22, 2025, at 09:09, Edmond Dantes wrote: Thanks for putting in the work to start to bring async support to PHP. I = have a few initial comments: * Is TrueAsync intended strictly to enable concurrency without = parallelism (e.g. single-threaded cooperative multitasking)? Is there a = future path in mind for parallelism (actual simultaneous execution)? * Following on from that question, I note that there is no way to = annotate types as being safely sendable across concurrent execution = contexts. PHP does have a thread-safe ZTS mode. And a long-term goal = should be to allow for parallel (multiple concurrent execution) = asynchronous tasks, especially if we want to expand asynchronous from = just being a way to accomplish other single-threaded work while waiting = on I/O. In a concurrent environment, one has to consider the effects of multiple = threads trying to read or write from the same value at once. (Strictly = speaking, even in a single-threaded world, that's not necessarily safe = to ignore; the stdclib or kernel code behind the scenes may be doing all = sorts of shenanigans with I/O behind everyone's back, though I suspect = an async-aware PHP could itself be a sufficient buffer between that and = userspace code.) With multithreaded code, it's generally not safe to pass around types = unless you can reason about whether it's actually safe to do so. This = could be trivially encoded in the type system with, say, a marker = interface that declares that a type is safe to send across threads. But = this needs to be opt-in, and the compiler needs to enforce it, or else = you lose all pretense of safety. It's tempting to handwave this now, but adding it in later might prove = intractable due to the implied BC break. E.g. trivially, any call to = spawn() might have its task scheduled on a different thread, but you = can't do that safely unless you know all types involved are thread-safe. = And while you could determine that at runtime, that's far more expensive = and error-prone than making the compiler do it. That said: the TrueAsync proposal isn't proposing async/await keywords. = Perhaps it's sufficient to say that "TrueAsync" is single-thread only, = and the multithreaded case will be handled with a completely different = API later so BC isn't an issue. But either way, this should be at least = mentioned in the Future Scope section. * Rob Landers brought up the question about multiple-value Awaitables. I = agree with him that it is a potential footgun. If an Awaitable is going = to return multiple values, then it should implement have a different = interface to indicate that contract. Returning multiple values is fundamentally different than returning one = value, and providing space for a contract change of that magnitude = without reflecting in the type system is likely to cause significant = problems as async interfaces evolve. Doing so would be like allowing a = function that is defined to return an int to actually return an array of = ints with no change to its type. That is clearly wrong. One should be = able to look at a type definition and tell from its interfaces whether = it is single-value or multiple-value. (For example: Swift's standard library provides the AsyncSequence = interface that one uses when wanting to asynchronously stream values. A = concrete AsyncStream type provides an implementation of AsyncSequence = that allows easily converting callback-based value streaming into an = asynchronous sequence that can be iterated on with a for await loop. You = don't await on an AsyncSequence type itself; instead you get an iterator = and use the iterator to get values until the iterator returns null or = throws an error. This is assisted with the syntactic sugar of a `for = await` loop.) Rob: The answer in Swift to what happens if multiple readers await a = non-idempotent Awaiter (e.g., there are multiple readers for an = AsyncSequence) is that as new values are made available, they go in turn = to whatever readers are awaiting. So if you have five readers of an = AsyncSequence, and the sequence emits ten values, they may all go to one = reader, or each reader might get two values, or any combination thereof, = depending on core count and how the scheduler schedules the tasks. So = this can be used to spread workload across multiple threads, but if you = need each reader to get all the values, you need to reach for other = constructs. I don't have a particular view on whether values should be replicated to = multiple readers or not, but I do find it a bit annoying that Swift's = standard library doesn't provide an easy way to get replicated values. = So whichever way PHP goes, it should be clearly documented, _and_ there = be an easy way to get the other behavior. * For cancellation, I'm confused on the resulting value. The section on = cancellation suggests ("If a coroutine has already completed, nothing = happens.") but does not explicitly state that an already-completed = coroutine will have its return value preserved. The section on = self-cancellation explicitly says that a self-cancelled coroutine throws = away its return value, despite ultimately successfully completing. This = seems counter-intuitive. Why should a self-cancellation be treated = differently than an external cancellation? If the task completes through = to the end and doesn't emit that it was cancelled, why should its return = value be ignored in some cases and preserved in others? I think self-cancellation is a weird edge case. It should probably be = prohibited like self-await is. In general, I think a coroutine shouldn't = ever have a handle to itself. Why would it need that? If a coroutine = needs to abort early, it should throw or emit a partial result directly, = rather than using the cancel mechanism. * exit/die called within a cooroutine is defined (to immediately = terminate the app). For clarity, the case when exit/die is called = outside of async code while there is a coroutine executing should also = be explicitly documented. (Presumably this means that the async code is = immediately terminated, but you don't state that, and Async\protect() = would suggest that maybe it doesn't always apply.) * I think Async\protect() and forcibly cancelling coroutines beyond = calling $coroutine->cancel() should be removed. Async\protect() is the = answer to "but I need to make sure that this gets completed when a task = is forcibly cancelled". But that just begs for a = $scope->reallyCancelEvenProtectedCoroutines() and a Async\superProtect() = to bypass that, and so on. Yes, this means that a coroutine might never complete. But this is no = different than non-async code calling a function that has an infinite = loop and never returns. * Aleksander Machniak brought up possibly merging suspend and delay. As = you note, they are semantically different (delay for a specified time = interval, vs. cooperatively yield execution for an unspecified time), = and I think they should stay separate. If we're bikeshedding, I might = rename suspend() to yield(), though that might not be possible with = yield being a language keyword. That could be worked around if yield() = was, say, a static method on Coroutine, so you would call = Coroutine::yield() to yield the current coroutine. (That could also = apply to isCancelled, so a particular coroutine would never need to have = a reference to itself to check if cancellation is requested.) * Also bikeshedding, but I would suggest renaming Coroutine to Task. = Task is shorter and much easier to say and type, but more importantly, = it encourages thinking about the effect (code that will eventually = return a value), rather than how that effect is achieved. It also means = the same abstraction can be used in a multithreaded environment. -John