====== PHP True Async ====== * Version: 1.7 * Date: 2025-12-22 * Author: Edmond [HT], edmondifthen@proton.me * Status: Under discussion * First Published at: http://wiki.php.net/rfc/true_async * Git: https://github.com/true-async * Related RFC: [[rfc:true_async_scope|PHP True Async: Scope and Structured Concurrency]] ===== Introduction ===== For several years, **PHP** has been attempting to carve out a niche in the development of long-running applications, where concurrent code execution becomes particularly useful. Production-ready solutions such as **Swoole**, **AMPHP**, **ReactPHP**, and others have emerged. However, **PHP** still does not provide a comprehensive implementation for writing concurrent code. PHP extensions have no way to support //non-blocking execution//, even if they are capable of doing so. **Swoole** is forced to copy thousands of lines of code just for a few modifications, while **AMPHP** developers have to build drivers for ''MySQL'', ''PostgreSQL'', ''Redis'', and other systems from scratch in user-land. The goal of this **RFC** is to establish a standard for writing concurrent code in PHP, as well as a C-API interface that would allow PHP to be extended at a low level using C, Rust, C++, and other languages. This would enable extensions to support **non-blocking I/O** without the need to override PHP functions or duplicate code. ===== Goals ===== The **True Async** project pursues the following goals and values: * From a PHP developer's perspective, the **main value** is **minimizing code changes** required to enable concurrency. Unlike explicit async models (where async/await keywords must be added everywhere), this approach allows reusing existing synchronous code inside coroutines with minimal modifications, provided that developers account for concurrency considerations such as shared mutable state. * Code running inside a Coroutine **maintains the illusion of sequential execution**. Developers can write straightforward, sequential logic without explicit yield points or async markers, while the runtime handles concurrency transparently. * Coroutine switching is managed automatically by the runtime. Developers do not need to manually control when and how coroutines switch, except when explicitly choosing to intervene (e.g., using `suspend()`). * The API design prioritizes familiarity and adopts patterns recognizable to developers from existing ecosystems (AMPHP, Go coroutines, Swoole). * The design balances flexibility and simplicity by providing essential concurrency primitives in the language core, eliminating dependency on external libraries while avoiding unnecessary complexity. ===== Proposal ===== ==== Overview ==== === Short glossary === | Term | Description | Section | | **Coroutine** | An executable unit of code that can be suspended and resumed | Launching any function in non-blocking mode | | **Cancellation** | A mechanism for cooperative canceling coroutine execution | Cancellation | | **Awaitable** | An interface for objects that can be awaited (may produce different values) | Awaitable interface | | **Completable** | An interface for single-assignment awaitables with idempotent reads | Completable interface | | **Suspension** | The state when a coroutine pauses execution and yields control | Suspension | | **Graceful Shutdown** | A safe application termination mode that cancels all coroutines | Graceful Shutdown | | **Deadlock** | A condition where no coroutines can progress due to circular dependencies | Deadlocks | | **Scheduler** | A component that manages coroutine execution and switching | Coroutine lifecycle | | **Reactor** | An event loop that handles I/O events and timers | Coroutine lifecycle | This RFC introduces native support for concurrent programming in ''PHP'' through coroutines and async/await primitives. **What is proposed (new functionality):** * New ''Async'' namespace with coroutine functions and classes * ''spawn()'' function to create coroutines * ''await()'' function to wait for coroutine results * ''suspend()'' function to yield control between coroutines * ''Coroutine'' class representing an executable unit * ''Awaitable'' and ''Completable'' interfaces for awaitable objects * ''Cancellation'' exception for cooperative cancellation * Graceful shutdown mechanism and deadlock detection **What is left untouched (no changes to existing function contracts):** * Existing PHP functions like ''file_get_contents()'', ''fopen()'', ''mysqli_query()'' **keep their signatures and contracts unchanged** * No modifications needed to existing synchronous code to work inside coroutines * No breaking changes to existing PHP extensions or SAPIs This **RFC** defines the general rules for all input/output functions, but the exact behavior of specific I/O functions will be described in separate RFCs if needed. **What becomes incompatible:** * New root namespace class `\Cancellation` may conflict with existing userland code === API Overview === Examples: === Coroutine === A lightweight execution thread that can be suspended (''suspend'') and resumed. Example use function Async\spawn; use function Async\suspend; spawn(function() { echo "Start"; suspend(); // Suspend the coroutine echo "Resumed"; }); === Waiting for coroutine results === use function Async\spawn; use function Async\await; function fetchData(string $file): string { $result = file_get_contents($file); if($result === false) { throw new Exception("Error reading $file"); } return $result; } echo await(spawn(fetchData(...), "file.txt")); === Awaiting a result with cancellation === use function Async\spawn; use function Async\await; echo await(spawn(file_get_contents(...), "https://php.net/"), spawn('sleep', 2)); === Suspend === Transferring control from the coroutine to the other coroutines.: use function Async\spawn; use function Async\suspend; function myFunction(): void { echo "Hello, World!\n"; suspend(); echo "Goodbye, World!\n"; } spawn(myFunction(...)); echo "Next line\n"; Output Hello, World Next line Goodbye, World ==== Cancellable by design ==== This **RFC** is based on the principle of **"Cancellable by design"**, which can be described as follows: > By default, coroutines **should be** designed in such a way that their > cancellation at any moment does not compromise data integrity (for example, the Database). In other words, by default, I/O operations or others awaiting operations, **can be cancelled** by code **outside** the coroutine at **any moment**. To properly complete transactions and release resources, coroutine code **MUST** handle the cancellation exception ''Cancellation''. === Rationale === This **"cancellable by default"** policy is particularly well-suited for PHP because **read operations** (database queries, API calls, file reads) are typically as frequent as—or even more frequent than—write operations. Since read operations generally don't modify state, they're inherently safe to cancel without risking data corruption. By making all operations cancellable by default, this approach eliminates the need for developers to explicitly mark or protect individual read operations, significantly reducing boilerplate code. Only write operations and critical sections that require transactional guarantees need special handling through proper ''Cancellation'' exception management. ==== Namespace ==== All functions, classes, and constants defined in this **RFC** are located in the ''Async'' namespace (except for `Cancellation`). Extensions are allowed to extend this namespace with functions and classes, provided that they are directly related to concurrency functionality. ==== Scheduler and Reactor ==== **Scheduler** and **Reactor** are internal components: - The **scheduler** is responsible for the execution order of coroutines. - The **reactor** is responsible for input/output events. ==== Coroutine ==== > A ''Coroutine'' is an ''execution container'', transparent to the code, > that can be suspended on demand and resumed at any time. The ''Coroutine'' class implements the ''Completable'' interface, meaning it completes once and preserves its final result (value or exception). Isolated execution contexts make it possible to switch between coroutines and execute tasks concurrently. Any function can be executed as a coroutine without any changes to the code. A coroutine can stop itself passing control to the ''scheduler''. However, it cannot be stopped externally. A suspended coroutine can be resumed at any time. The ''scheduler'' component is responsible for the coroutine resumption algorithm. A coroutine can be resumed with an **exception**, in which case an exception will be thrown from the suspension point. === Coroutine Lifecycle === {{ :rfc:true_async:coroutine-lifecycle.svg |}} This state diagram illustrates the lifecycle of a coroutine, showing how it transitions through various states during its execution: **States:** - **Created** – The coroutine has been defined but not yet started. - **Queued** – The coroutine is queued - **Running** – The coroutine is actively executing. - **Suspended** – Execution is paused, usually waiting for a result or I/O. - **Completed** – The coroutine has finished (Successfully or with an exception). - **Pending Cancellation** – A cancellation was requested; the coroutine is cleaning up. **Key Transitions:** - ''spawn'' moves a coroutine from **Created** to **Running**. - ''suspend'' and ''resume'' move it between **Running** and **Suspended**. - ''return/exit'' ends it in **Completed**. - ''cancel()'' initiates cancellation from **Running** or **Suspended**, leading to **Pending Cancellation**, and finally **Complete** with **Cancelled** flag. === ''Coroutine'' state check methods === | Method | Description | Related State on Diagram | | ''isStarted(): bool'' | Returns ''true'' if the coroutine has been started. | ''Running'', ''Suspended'', etc. | | ''isRunning(): bool'' | Returns ''true'' if the coroutine is currently running. | ''Running'' | | ''isQueued(): bool'' | Returns ''true'' if the coroutine is queued. | ''Queued'' | | ''isSuspended(): bool'' | Returns ''true'' if the coroutine is suspended. | ''Suspended'' | | ''isCancelled(): bool'' | Returns ''true'' if the coroutine has been cancelled. | ''Cancelled'' | | ''isCancellationRequested(): bool'' | Returns ''true'' if cancellation has been requested. | ''Pending Cancellation'' | | ''isCompleted(): bool'' | Returns ''true'' if the coroutine has completed execution. | ''Completed'', ''Cancelled'' | ==== Spawn function ==== To create coroutines, the ''spawn(callable $callable, mixed ...$args)'' function is used. It launches the '''' in a separate execution context and returns an instance of the ''Async\Coroutine'' class as a result. Let's look at two examples: > **Note:** The examples below are for demonstration purposes only. > The non-blocking version of the ''file_get_contents'' function is not part of this **RFC**. $result = file_get_contents('https://php.net'); echo "next line".__LINE__."\n"; This code - first returns the contents of the PHP website, - then executes the ''echo'' statement. use function Async\spawn; $coroutine = spawn('file_get_contents', 'https://php.net'); echo "next line".__LINE__."\n"; This code - starts a coroutine with the ''file_get_contents'' function. - The next line is executed without waiting for the result of ''file_get_contents''. - The coroutine is executed after the ''echo'' statement. ==== Suspension ==== A coroutine can suspend itself at any time using the ''suspend'' keyword: use function Async\spawn; use function Async\suspend; function example(string $name): void { echo "Hello, $name!"; suspend(); echo "Goodbye, $name!"; } spawn('example', 'World'); spawn('example', 'Universe'); Expected output Hello, World! Hello, Universe! Goodbye, World! Goodbye, Universe! With ''suspend()'', a coroutine yields control to the ''Scheduler'', which decides what to do next. The exact decision made by the ''Scheduler'' is not part of this **RFC**, and the scheduling algorithm may change at any moment (this algorithm may also be modified by a third-party extension). > A developer **MUST NOT** try to guess when a coroutine will resume execution or which other coroutine will be scheduled! > Even if such information is obtained through testing or by reading the current implementation of the ''Scheduler'', > the behavior may change at any moment! **Basic usage:** use function Async\suspend; suspend(); **Exceptions:** * ''suspend()'' can throw ''Cancellation'' if the **current** coroutine is cancelled while suspended * The cancellation is initiated externally via ''Coroutine::cancel()'' The ''suspend'' can be used in any function including from the **main execution flow**: use function Async\spawn; use function Async\suspend; function example(string $name): void { echo "Hello, $name!"; suspend(); echo "Goodbye, $name!"; } $coroutine = spawn(example(...), 'World'); // suspend the main flow suspend(); echo "Back to the main flow"; Expected output Hello, World! Back to the main flow Goodbye, World! The ''suspend'' keyword can be a throw point if someone resumes the coroutine externally with an exception. function example(string $name): void { echo "Hello, $name!"; try { suspend(); } catch (Exception $e) { echo "Caught exception: ", $e->getMessage(); } echo "Goodbye, $name!"; } $coroutine = spawn('example', 'World'); // pass control to the coroutine suspend(); $coroutine->cancel(); **Expected output** Hello, World! Caught exception: cancelled at ... Goodbye, World! ==== Awaitable interface ==== The ''Awaitable'' interface describes objects that represent event sources that can be awaited. The ''Awaitable'' interface does not impose limitations on the number of state changes. In the general case, objects implementing the ''Awaitable'' interface can act as triggers — that is, they can change their state an unlimited number of times. This means that multiple calls to ''select '' may produce different results. ==== Completable interface ==== The ''Completable'' interface extends ''Awaitable'' and represents objects with **single-assignment semantics** — they complete once with a result and provide **idempotent reads**. interface Async\Completable extends Async\Awaitable { public function cancel(?Cancellation $cancellation = null): void; public function isCompleted(): bool; public function isCancelled(): bool; } Unlike the base ''Awaitable'' interface which allows multiple state changes, ''Completable'' implements single-assignment semantics: * **Single-assignment**: The result is set exactly once (write-once) * **Idempotent reads**: Multiple ''await()'' calls always return the same result ==== Await ==== Async\await(Async\Completable $awaitable, ?Async\Completable $cancellation = null): mixed The ''await'' function is used to wait for the completion of another coroutine or any object that implements the ''Completable'' interface. use function Async\spawn; use function Async\await; function readFile(string $fileName):string { $result = file_get_contents($fileName); if($result === false) { throw new Exception("Error reading file1.txt"); } return $result; } $coroutine = spawn(readFile(...), 'file1.txt'); echo await($coroutine); // or echo await(spawn(readFile(...), 'file2.txt')); ''await'' suspends the execution of the current coroutine until the awaited one returns a final result or completes with an exception. **Exceptions:** * Throws ''Cancellation'' if the **current** coroutine (the one calling ''await'') is cancelled while waiting * Re-throws any exception that occurred in the awaited coroutine, **except** ''Cancellation'' which is always suppressed by the coroutine > **Coroutines behave like Futures:** > once a coroutine completes (successfully, with an exception, or through cancellation), > it preserves its final state. > Multiple calls to ''await()'' on the same coroutine will always return the same result or > throw the same exception. use function Async\spawn; use function Async\await; function testException(): void { throw new Exception("Error"); } try { await(spawn(testException(...))); } catch (Exception $e) { echo "Caught exception: ", $e->getMessage(); } === Await with cancellation === The ''await'' function can accept a second argument ''$cancellation'', which is an ''Awaitable'' object. This object can be a ''Coroutine'', or another object that implements the ''Awaitable'' interface. The $cancellation argument limits the waiting time for the first argument. As soon as the $cancellation is triggered, execution is interrupted with an exception ''AwaitCancelledException''. use function Async\spawn; use function Async\await; use Async\AwaitCancelledException; function readFile(string $fileName):string { $result = file_get_contents($fileName); if($result === false) { throw new Exception("Error reading file1.txt"); } return $result; } $cancellation = spawn(function() { sleep(2); }); try { // Wait for the coroutine to finish or for the cancellation to occur echo await(spawn(readFile(...), 'file1.txt'), $cancellation); } catch (AwaitCancelledException $e) { echo "Caught exception: ", $e->getMessage(); } ==== Fiber Support ==== **Fibers** are a type of **coroutine** that work as ''stackful generators'' with symmetric behavior. **Key principles:** * **Fiber** is a coroutine that can be suspended and resumed at any time (see ''Async\suspend()''). Fibers fully inherit coroutine behavior. * ''Fiber::start()'' and ''Fiber::resume()'' **block** the calling coroutine * The methods ''Fiber::start()'' and ''Fiber::resume()'' return control to the coroutine from which they were called. * ''Fibers'' can be suspended and resumed multiple times * Each ''Fiber'' has an associated coroutine (accessible via ''Fiber::getCoroutine()'') === Basic Usage === use function Async\spawn; use function Async\await; $coroutine = spawn(function() { // Create a fiber inside coroutine $fiber = new Fiber(function() { echo "Fiber executing\n"; return "fiber result"; }); // start() blocks until fiber completes or suspends $result = $fiber->start(); echo "Result: $result\n"; }); await($coroutine); === Fiber YIELD state === Fibers have a special ''YIELD'' state, which means that the fiber was suspended using a method ''Fiber::suspend''. The ''YIELD'' state indicates that the fiber is waiting to be resumed using a method ''Fiber::resume()''. $coroutine = spawn(function() { $fiber = new Fiber(function() { echo "Before suspend\n"; $value = Fiber::suspend("suspended value"); echo "Resumed with: $value\n"; return "done"; }); $suspended = $fiber->start(); // Returns "suspended value" echo "Fiber suspended with: $suspended\n"; $result = $fiber->resume("resume value"); // Returns "done" echo "Fiber returned: $result\n"; }); await($coroutine); Fibers maintain symmetric behavior **relative to the coroutine where they were created**. This means that if Fibers are created in two different coroutines, they are not required to be symmetric with each other; only within their own coroutine's context. // Two coroutines, each with its own Fiber $c1 = spawn(function() { $fiber = new Fiber(function() { echo "C1-Fiber: step 1\n"; Fiber::suspend(); echo "C1-Fiber: step 2\n"; }); $fiber->start(); echo "C1: between calls\n"; $fiber->resume(); }); $c2 = spawn(function() { $fiber = new Fiber(function() { echo "C2-Fiber: step 1\n"; Fiber::suspend(); echo "C2-Fiber: step 2\n"; }); $fiber->start(); echo "C2: between calls\n"; $fiber->resume(); }); await($c1); await($c2); // Each Fiber is symmetric only within its own coroutine // They execute independently and don't interfere with each other Like coroutines, fibers can be suspended using ''Async\suspend()''. In this case, control is transferred to the ''Scheduler'', not to the coroutine that started the Fiber. An attempt to resume the fiber using ''Fiber::resume()'', that was suspended using ''Async\suspend()'' will result in an exception ''FiberError'' with the message ''Cannot resume a not yielded fiber''. ''Fiber::resume()'' may throw an exception ''Cancellation'' exception if the Fiber-coroutine is cancelled. === Fiber Ownership === Coroutines typically don't just stop execution arbitrarily, they **await I/O events**. Fibers, however, can remain in a **"suspended state"** waiting for someone to call ''Fiber::resume()''. This is a fundamental distinction. **Ownership model:** * Coroutines belong to the **Scheduler**, which manages and tracks them * Fibers belong to **objects/variables** that hold their reference * While the object is alive, the ''Fiber'' can remain in ''suspend'' state indefinitely * The object essentially **"holds"** the Fiber in suspended state * When the object is destroyed (reference count reaches zero), the Fiber's coroutine seeks to terminate execution $coroutine = spawn(function() { $fiber = new Fiber(function() { echo "Step 1\n"; Fiber::suspend(); // Fiber waits for explicit resume echo "Step 2\n"; Fiber::suspend(); // Still waiting echo "Step 3\n"; }); $fiber->start(); // Execute until first suspend // $fiber variable OWNS the suspended fiber // The Fiber waits here, not in scheduler queue $fiber->resume(); // Explicit resume $fiber->resume(); // Another explicit resume // When $fiber goes out of scope, Fiber's coroutine terminates }); **Fiber destruction behavior:** When the ''Fiber'' object is destroyed (reference count reaches zero), the Fiber's coroutine resumes in a **special mode** where only ''finally'' blocks are executed. This behavior matches the current ''Fiber'' termination semantics and differs from explicit cancellation. $coroutine = spawn(function() { $fiber = new Fiber(function() { try { echo "Step 1\n"; Fiber::suspend(); echo "Step 2 - never executes\n"; // Won't execute } finally { echo "Cleanup in finally\n"; // Will execute } }); $fiber->start(); // $fiber goes out of scope here // Fiber's coroutine resumes only to execute finally blocks }); This ensures proper cleanup of resources when Fiber objects are garbage collected, while preventing execution of normal code paths. === Accessing Fiber's Coroutine === $coroutine = spawn(function() { $fiber = new Fiber(function() { Fiber::suspend(); return "done"; }); $fiber->start(); // Get fiber's associated coroutine $fiberCoroutine = $fiber->getCoroutine(); echo "Coroutine ID: " . $fiberCoroutine->getId() . "\n"; echo "Is suspended: " . ($fiberCoroutine->isSuspended() ? "yes" : "no") . "\n"; // Can cancel fiber via its coroutine $fiberCoroutine->cancel(new \Cancellation("cancelled")); }); The ''Fiber::getCoroutine()'' method returns the associated coroutine even **after the Fiber has terminated**. This allows inspection of the Fiber's final state and access to its result or exception. === Deadlock Resolution for Fibers === The ''Scheduler'' takes the special ''YIELD'' state into account when detecting a deadlock. **Resolution strategy:** If **all remaining coroutines** are Fiber-coroutines in ''YIELD'' state, the scheduler performs **graceful cancellation** instead of throwing ''DeadlockCancellation''. This happens because: * Suspended Fibers belong to their variables/objects, not to the scheduler * They are waiting for explicit resume, which is a valid suspended state * This is not a true deadlock. It's intentional symmetric suspension **Behavior difference:** * **Regular coroutines in deadlock**: Receive ''DeadlockCancellation'' * **Only Fiber-coroutines suspended**: Graceful cancellation without exception use function Async\spawn; use function Async\await; $coro = spawn(function() { $fiber = new Fiber(function() { Fiber::suspend(); // Waiting for resume // If no resume happens and this is the only coroutine left, // graceful cancellation occurs instead of DeadlockCancellation }); $fiber->start(); // Coroutine ends without resuming fiber // This triggers graceful cancellation, not deadlock error }); await($coro); // No DeadlockCancellation thrown This design recognizes that suspended Fibers represent intentional state, not erroneous deadlock conditions. ==== Edge Behavior ==== The use of ''spawn''/''await''/''suspend'' is allowed in any part of a PHP program. This is possible because the PHP script entry point forms the **main execution thread**, which is also considered a coroutine. As a result, operations like ''suspend'' and ''current_coroutine()'' will behave the same way as in other cases. Asynchronous code will work as expected, including inside ''register_shutdown_function''. === Memory Management and Garbage Collection === **Coroutines** participate in PHP's standard garbage collection system: * **Unreferenced coroutines** can be collected by the garbage collector * **Circular references** between objects and coroutines are properly detected and resolved * **Active coroutines** (running or suspended) are protected from collection while they have pending operations Coroutines retain either the execution result or the exception that occurred. These items are visible to the garbage collector. === Destructors and Async Operations === **Destructors can contain async operations** and execute asynchronously: use function Async\spawn; use function Async\suspend; class AsyncResource { public function __destruct() { // Destructors can spawn coroutines spawn(function() { echo "Async cleanup\n"; }); // Destructors can suspend suspend(); } } Destructor execution is **deferred** until the async context allows it, and destructors run **asynchronously** like other async code. ==== finally ==== The ''finally'' method allows defining a callback function that will be invoked when a coroutine completes. This method can be considered a direct analog of ''defer'' in Go. > ⚠️ **Important:** All ''finally'' handlers are executed concurrently in separate coroutines. > This ensures that slow handlers do not block the completion of other handlers or the main execution flow. For ''Coroutine'', the callback receives the completed coroutine as a parameter: use function Async\spawn; use Async\Coroutine; function task(): void { throw new Exception("Task 1"); } $coroutine = spawn('task'); $coroutine->finally(function (Coroutine $completedCoroutine) { echo "Coroutine " . spl_object_id($completedCoroutine) . " completed\n"; }); The ''finally'' semantics are most commonly used to release resources, serving as a shorter alternative to ''try-finally'' blocks: function task(): void { $file = fopen('file.txt', 'r'); finally(fn() => fclose($file)); throw new Exception("Task 1"); } spawn('task'); ==== Cancellation ==== The ''cancellation'' operation allows interrupting the execution of a coroutine that is in a waiting state. A cancelled coroutine is **resumed** with a special ''Cancellation'' exception and continues its execution. The cancellation operation is not instantaneous; it takes time. While in the ''cancellation'' state, a coroutine may execute as usual without any restrictions. The cancellation operation is available for coroutines using the ''cancel()'' method: function task(): void {} $coroutine = spawn(task(...)); // cancel the coroutine $coroutine->cancel(new \Cancellation('Task was cancelled')); The cancellation operation is implemented as follows: - If a coroutine has not started, it will never start. - If a coroutine is suspended, its execution will resume with an exception. - If a coroutine has already completed, nothing happens. The ''Cancellation'', if unhandled within a coroutine, is automatically suppressed after the coroutine completes. However, the coroutine preserves its cancellation state, and any ''await()'' operation on this coroutine will throw the ''Cancellation''. > ⚠️ **Warning:** You should not attempt to suppress ''Cancellation'' exception, > as it may cause application malfunctions. > > **Note:** ''Cancellation'' can be extended by the user > to add metadata that can be used for debugging purposes. > === Exception override rules === **Cancellation** exceptions do not override each other. Multiple ''cancel()'' calls preserve the first cancellation reason: $coroutine = spawn(function() { // some work }); $coroutine->cancel(new \Cancellation("First reason")); $coroutine->cancel(new \Cancellation("Second reason")); // ignored await($coroutine); // throws Cancellation("First reason") However, if an unhandled exception occurs during cancellation that is **not** inherited from ''Cancellation'', it overrides the previous ''Cancellation'': $coroutine = spawn(function() { // coroutine gets cancelled, but then throws different exception throw new \RuntimeException("boom"); }); $coroutine->cancel(new \Cancellation("Cancelled")); await($coroutine); // throws RuntimeException, not Cancellation === Cancellation policy === This **RFC** intentionally does not define rules for tracking the execution time of cancelled coroutines. The reason is that cancellation operations may be long-running—for example, rollback strategies—and may require blocking the function being cancelled. Intentionally stopping coroutines that are in the cancellation state is a dangerous operation that can lead to data loss. To avoid overcomplicating this **RFC**, it is proposed to delegate the responsibility for such logic to the ''scheduler'' implementation. === Self-cancellation === A coroutine can attempt to cancel itself by calling its own ''cancel()'' method. However, **nothing happens immediately** - the coroutine continues executing normally until it naturally completes, but is marked as cancelled and stores the ''Cancellation'' as its final result: use function Async\spawn; $coroutine = spawn(function() use (&$coroutine) { $coroutine->cancel(new \Cancellation("Self-cancelled")); echo "This still executes\n"; // Will execute return "completed"; }); // await() will throw Cancellation despite normal completion Self-cancellation only sets the exception as the coroutine result, but does not throw it immediately. For example, ''suspend()'' can be called without triggering an exception: $coroutine = spawn(function() use (&$coroutine) { $coroutine->cancel(new \Cancellation("Self-cancelled")); suspend(); // No exception thrown here echo "After suspend\n"; }); === Cancellation handling === In the context of coroutines, it is not recommended to use ''catch \Throwable'' or ''catch Cancellation''. Since ''Cancellation'' does not extend the ''\Exception'' class, using ''catch \Exception'' is a safe way to handle exceptions, and the ''finally'' block is the recommended way to execute finalizing code. use function Async\spawn; use function Async\await; try { $coroutine = spawn(function() { await(spawn(sleep(...), 1)); throw new \Exception("Task 1"); }); spawn(function() use ($coroutine) { $coroutine->cancel(); }); try { await($coroutine); } catch (\Exception $exception) { // recommended way to handle exceptions echo "Caught exception: {$exception->getMessage()}\n"; } } finally { echo "The end\n"; } Expected output The end try { $coroutine = spawn(function() { await(spawn(sleep(...), 1)); throw new \Exception("Task 1"); }); spawn(function() use ($coroutine) { $coroutine->cancel(); }); try { await($coroutine); } catch (\Cancellation $exception) { // not recommended way to handle exceptions echo "Caught Cancellation\n"; throw $exception; } } finally { echo "The end\n"; } Expected output Caught Cancellation The end === Cancellation propagation === The ''Cancellation'' affects **PHP** standard library functions differently. If it is thrown inside one of these functions that previously did not throw exceptions, the PHP function will terminate with an error. In other words, the ''cancel()'' mechanism does not alter the existing function contract. PHP standard library functions behave as if the operation had failed. Additionally, the ''Cancellation'' will not appear in ''get_last_error()'', but it may trigger an ''E_WARNING'' to maintain compatibility with expected behavior for functions like ''fwrite'' (if such behavior is specified in the documentation). ==== Graceful Shutdown ==== When an **unhandled exception** occurs in a **Coroutine** the **Graceful Shutdown** mode is initiated. Its goal is to safely terminate the application. **Graceful Shutdown** cancels all coroutines, then continues execution without restrictions, allowing the application to shut down naturally. **Graceful Shutdown** does not prevent the creation of new coroutines or close connection descriptors. However, if another unhandled exception is thrown during the **Graceful Shutdown** process, the second phase is triggered. **Second Phase of Graceful Shutdown** - All **Event Loop descriptors** are closed. - All **timers** are destroyed. - Any remaining coroutines that were not yet canceled will be **forcibly canceled**. The further shutdown logic may depend on the specific implementation of the **Scheduler** component, which can be an external system and is not covered by this **RFC**. The **Graceful Shutdown** mode can also be triggered using the function: Async\shutdown(?Cancellation $cancellation = null): void {} from anywhere in the application. === exit and die keywords === The ''exit''/''die'' keywords always trigger the **Graceful Shutdown** mode, regardless of where they are called in the code. This works the same way as when an unhandled exception occurs. **Graceful Shutdown** allows the application to safely terminate by canceling all coroutines and performing necessary cleanup operations. Unlike the ''cancel()'' operation, ''exit''/''die'' terminates the entire application, not just the current coroutine. === Deadlocks === A situation may arise where there are no active **Coroutines** in the execution queue and no active handlers in the event loop. This condition is called a **Deadlock**, and it represents a serious logical error. When a **Deadlock** is detected, the application enters **Graceful Shutdown** mode and generates warnings containing information about which **Coroutines** are in a waiting state and the exact lines of code where they were suspended. At the end of the application lifecycle, if a deadlock condition was detected, a **DeadlockCancellation** exception is thrown as a fatal error. This exception extends **Error** and indicates a critical architectural problem in the application design. The **DeadlockCancellation** is not intended to be caught during normal execution, but rather serves as a diagnostic tool to identify circular dependencies between coroutines. Example of deadlock situation: use function Async\spawn; use function Async\await; use function Async\suspend; $coroutine1 = spawn(function() use (&$coroutine2) { suspend(); await($coroutine2); // Waits for coroutine2 }); $coroutine2 = spawn(function() use ($coroutine1) { suspend(); await($coroutine1); // Waits for coroutine1 - deadlock! }); // Results in: Fatal error: Uncaught Async\DeadlockCancellation: // Deadlock detected: no active coroutines, 2 coroutines in waiting ==== Tools ==== The ''Coroutine'' class implements methods for inspecting the state of a coroutine. | Method | Description | | **''getSpawnFileAndLine():array''** | Returns an array of two elements: the file name and the line number where the coroutine was spawned. | | **''getSpawnLocation():string''** | Returns a string representation of the location where the coroutine was spawned, typically in the format ''"file:line"''. | | **''getSuspendFileAndLine():array''** | Returns an array of two elements: the file name and the line number where the coroutine was last suspended. If the coroutine has not been suspended, it may return empty string,0. | | **''getSuspendLocation():string''** | Returns a string representation of the location where the coroutine was last suspended, typically in the format ''"file:line"''. If the coroutine has not been suspended, it may return an empty string. | | **''isSuspended():bool''** | Returns ''true'' if the coroutine has been suspended | | **''isCancelled():bool''** | Returns ''true'' if the coroutine has been cancelled, otherwise ''false''. | The ''Coroutine::getAwaitingInfo()'' method returns an array with debugging information about what the coroutine is waiting for, if it is in a waiting state. The format of this array depends on the implementation of the **Scheduler** and the **Reactor**. The ''Async\get_coroutines()'' method returns an array of all coroutines in the application. ===== Backward Incompatible Changes ===== This RFC introduces **''Cancellation''** class in the root namespace: * ''Cancellation'' extends ''\Throwable'' directly, making it a fundamental PHP mechanism * It can be used by any PHP extension, not just Async * Similar to ''Error'', ''Exception'', and other core throwables **Potential conflicts:** * If userland code already defines ''Cancellation'' in root namespace, it will conflict * This is similar to how PHP 7.0 introduced ''Error'' class - rare but possible conflict * Mitigation: Check for existing ''Cancellation'' class before upgrading to PHP 8.6+ ==== Fiber API Extension ==== This RFC extends PHP's native **''Fiber''** class with new method: class Fiber { public function getCoroutine(): ?\Async\Coroutine {} } **BC Impact:** * **No breaking changes** - only adds new method to existing Fiber class * Fibers continue to work as before when True Async is not active * When True Async is active, Fibers get additional functionality * ''getCoroutine()'' returns ''null'' when fiber runs outside async context **Compatibility:** * Existing Fiber-based libraries continue to work without modifications * Libraries can optionally detect async context via ''getCoroutine() !== null'' * No changes needed to existing Fiber usage patterns ===== Proposed PHP Version(s) ===== PHP 8.6+ ===== RFC Impact ===== ==== To SAPIs ==== The use of asynchrony is **always available**, regardless of how `PHP` is integrated. The **True Async** module activates the reactor within the context of ''php_request_startup\php_request_shutdown()'' request processing. From the perspective of external integration, `PHP`’s behavior **remains unchanged**. This means that for all execution modes: from `FPM` to `CLI` asynchrony **does not require** any modifications to the SAPI code. ==== To Existing Extensions ==== No changes are expected in existing extensions. ==== To Opcache ==== Does not affect. ==== New Constants ==== No new constants are introduced. ==== To the Ecosystem ==== **Static Analyzers:** Static analyzers will need to implement new checks for safe async/await usage: - **Deadlock detection:** Identify potential deadlocks in async code - **Cancellation suppression detection:** ''catch (\Throwable)'' that may hide Cancellation **Libraries Using Fiber:** Libraries that use the ''Fiber API'' will continue to work as expected. There may be behavior changes related to transparent asynchrony. **Application Servers:** **FrankenPHP + TrueAsync** integration demonstrates how an application server can handle **multiple concurrent HTTP requests in a single thread** using coroutines. Each incoming request spawns a lightweight coroutine, enabling I/O multiplexing without blocking. Example: ''https://github.com/dunglas/frankenphp/blob/trueasync/examples/async_entrypoint.php'' ==== php.ini Defaults ==== No new php.ini directives are introduced. ===== Open Issues ===== None. ===== Future Scope ===== This RFC focuses on core async functionality (coroutines, await, cancellation). For `Scope` and structured concurrency, please see the [[rfc:true_async_scope|Scope RFC]]. === Reactor === At the moment, the Reactor component uses the ''LibUV'' library. This component should be implemented as a separate extension. ===== Voting Choices ===== Yes or no vote. 2/3 required to pass. Voting ends 2026-02-01 at 00:00:00 UTC * Yes * No * Abstain ===== Patches and Tests ===== * Current implementation: https://github.com/true-async ===== References ===== Links to external references, discussions or RFCs * **First Discussion** - https://externals.io/message/126402 * **Second Discussion** - https://externals.io/message/126537 * **Third Discussion** - https://externals.io/message/127120 * **Fourth Discussion** - https://externals.io/message/128777 * **Fifth Discussion** - https://externals.io/message/129004 * **TrueAsync API RFC** - https://github.com/true-async/php-true-async-rfc/blob/main/true-async-api-rfc.md Community discussions: * **Memory model and its impact on refactoring** - https://github.com/true-async/php-true-async-rfc/discussions/8 * **Async use cases** - https://github.com/true-async/php-true-async-rfc/discussions/9 * **Report on the feasibility of memory separation for static and global variables** - https://github.com/true-async/php-true-async-rfc/discussions/12 * **What if WordPress could be launched as a stateful application?** - https://github.com/true-async/php-true-async-rfc/discussions/16 Additional links: * [[https://github.com/EdmondDantes/php-true-async-rfc/blob/main/comparison.md|Comparison of concurrency models in programming languages]] * [[https://alejandromp.com/development/blog/the-importance-of-cooperative-cancellation/|Cooperative cancellation]] * [[https://pure.tudelft.nl/ws/portalfiles/portal/222760871/LIPIcs.ECOOP.2024.8.pdf|Understanding Concurrency Bugs in Real-World Programs with Kotlin Coroutines]] * [[https://dl.acm.org/doi/10.1145/3297858.3304069|Understanding Real-World Concurrency Bugs in Go]] * [[https://arxiv.org/abs/1901.03575|Static Analysis for Asynchronous JavaScript Programs]] The following can be considered as competing solutions to the current implementation: * **Swoole** (https://github.com/swoole/swoole-src) – a C++ library that implements a full feature set for concurrent programming. The advantage and disadvantage of **Swoole** is that it is a standalone solution that does not directly affect the language itself. * The **Swow** (https://github.com/swow/) project is a C library that provides a good lightweight API while not affecting the language itself and not requiring changes to PHP. * Examples of code: https://github.com/EdmondDantes/php-true-async-rfc/tree/main/examples ===== Implementation ===== The current implementation of the project is located here: https://github.com/true-async The code will be split into several PRs upon agreement. ===== Changelog ===== ==== Version 1.7 (December 2025) ==== * **Fiber integration:** added ''Fiber::getCoroutine()'' method, ownership model, deadlock resolution; BC BREAK: destructors run in scheduler context, not in Fibers * Added Coroutine methods: ''getId()'', ''getResult()'', ''getException()'', ''getTrace()'' * Refined Goals section: improved clarity and technical accuracy of core value propositions * Function naming compliance with PHP coding standards: renamed ''currentCoroutine()'' to ''current_coroutine()'', ''gracefulShutdown()'' to ''shutdown()'', ''getCoroutines()'' to ''get_coroutines()'' * Method renaming: ''Coroutine::onFinally()'' renamed to ''Coroutine::finally()'' for brevity * Interface renaming: ''FutureLike'' renamed to ''Completable'' (66% community vote) for clearer semantics; represents objects that can be completed * Exception renaming: ''CancellationError'' → ''Cancellation'' for better semantics * Moved ''Cancellation'' to **root namespace** (''\Cancellation'') as it's a fundamental PHP mechanism, not Async-specific * Changed ''Cancellation'' inheritance from ''\Error'' to ''\Throwable'' directly, following Python's ''CancelledError'' pattern (BaseException since 3.8) * Added backward compatibility notes about new root namespace class * Rename ''DeadlockException'' to ''DeadlockCancellation'' ==== Version 1.6 (November 2025) ==== * I/O functions have earned the right to be covered in a separate **RFC**. * Add basic exception Async\Exception according to the https://github.com/php/policies/blob/main/coding-standards-and-naming.rst#throwables * Rename ''DeadlockError'' to ''DeadlockException'' ==== Version 1.5 (October 2025) ==== * Added ''Completable'' interface methods: ''cancel()'', ''isCompleted()'', ''isCancelled()'' * Renamed ''Coroutine::isFinished()'' to ''Coroutine::isCompleted()'' * Clarified exit/die behavior: always triggers Graceful Shutdown mode regardless of where called * Added rationale for "Cancellable by design" policy: explains why default cancellability reduces code complexity for read-heavy PHP workloads * RFC structure improvements: reorganized Cancellation section with proper subsections hierarchy * Moved "Coroutine lifetime" as subsection under Coroutine section * Extended glossary with Awaitable, Suspension, Graceful Shutdown, and Deadlock terms * Introduced ''Completable'' interface with single-assignment semantics and changed ''await()'' signature to accept ''Completable'' instead of Awaitable for type safety * Split RFC: Moved Scope and structured concurrency functionality to separate [[rfc:true_async_scope|Scope RFC]]. Base RFC now focuses on core async primitives (coroutines, await, cancellation) * Documented that ''await()'' throws ''Cancellation'' when current coroutine is cancelled * Documented that ''suspend()'' throws ''Cancellation'' when coroutine is cancelled while suspended * Added exception override rules: ''Cancellation'' exceptions don't override each other (first wins), but other exceptions override ''Cancellation'' * Clarified self-cancellation behavior with ''suspend()'': self-cancellation only sets result without throwing exception immediately ==== Version 1.4 (September 2025) ==== * Major RFC simplification - removed CoroutineGroup, Future and AwaitXX functions * Added Memory Management and Garbage Collection section * Documented self-cancellation behavior in coroutines and its impact on execution * Enhanced "Cancellable by design" principle documentation * Updated suspend function information and usage guidelines * Added Scope waiting functionality and extra descriptions ===== Rejected Features ===== Keep this updated with features that were discussed on the mail lists.