====== PHP True Async ====== * Version: 1.4 * Date: 2025-09-30 * 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 ===== 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** of this implementation is that they DO NOT NEED to change existing code (or if changes are required, they should be minimal) to enable concurrency. Unlike explicit async models, this approach lets developers reuse existing synchronous code inside coroutines without modification. * Code that was originally written and intended to run outside of a Coroutine must work **EXACTLY THE SAME** inside a Coroutine without modifications. * A PHP developer should not have to think about how Coroutine switch and should not need to manage their switching—except in special cases where they consciously choose to intervene in this logic. * If there is existing code or a familiar style, such as AMPHP interfaces, Go coroutines, Swoole API, and others, it is best to use what is most recognizable to a broad range of developers. * The goal is to find a balance between flexibility and simplicity. On one hand, the implementation should allow leveraging existing solutions without requiring external libraries. On the other hand, it should avoid unnecessary complexity. Many design choices in this implementation are driven by the desire to free developers from concerns about compatibility with "external libraries" in favor of a standardized approach. ===== 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 | | **Scope** | A container for managing the lifecycle of coroutines | Scope | | **Zombie coroutine** | A coroutine that continues execution after its Scope has been destroyed | Scope disposal | | **CancellationError** | A mechanism for cooperative canceling coroutine execution | Cancellation | | **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** describes the **API** for writing concurrent code in PHP, which includes: === 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"; }); === Scope === A container that manages coroutine lifetimes. Example use function Async\spawn; $scope = new Async\Scope(); $scope->spawn(function() { // Coroutine bound to $scope spawn(function() { // Coroutine bound to $scope }); }); // Dispose of the scope after 5 seconds sleep(5); $scope->disposeSafely(); === Cooperative cancellation === A special exception that implements cooperative cancellation: Example use function Async\spawn; use function Async\suspend; $coroutine = spawn(function() { try { Async\delay(1000); } catch (Async\CancellationError $e) { echo "Coroutine cancelled"; throw $e; // Re-throw to propagate cancellation } }); suspend(); $coroutine->cancel(); === 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/"), Async\timeout(2000)); 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 === Structured concurrency === use Async\Scope; function getUserProfile(int $userId): array { $scope = new Scope(); $profile = []; $scope->spawn(function() use ($userId, &$profile) { $profile['data'] = fetchUserData($userId); }); $scope->spawn(function() use ($userId, &$profile) { $profile['orders'] = fetchUserOrders($userId); }); $scope->spawn(function() use ($userId, &$profile) { $profile['settings'] = fetchUserSettings($userId); }); $scope->awaitCompletion(Async\timeout(30000)); return $profile; } ==== 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 ''CancellationError''. ==== Coroutine lifetime ==== > A coroutine launched without an **explicitly defined** Scope has an **unknown** lifetime. A coroutine created with spawn inherits the current Scope and its lifetime. If code calls a third-party library function (or another module) that uses ''spawn()'' internally, the library ends up creating a coroutine whose lifetime is unknown to it. The programmer **should carefully** consider such situations and determine whether an undefined coroutine lifetime is what they expect in that context. If the coroutine lifetime matters, it is recommended to explicitly define a ''Scope'' to clearly manage coroutine lifetimes. ==== Namespace ==== All functions, classes, and constants defined in this **RFC** are located in the ''Async'' namespace. 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. 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'' | | ''isFinished(): 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. === Spawn with scope === To create a coroutine in a specific ''Scope'', you need to use the ''Scope::spawn'' method.: $scope = new Async\Scope(); // Launch a coroutine in the $scope $coroutine = $scope->spawn(function():string { return gethostbyname('php.net'); }); function defineTargetIpV4(string $host): string { return gethostbyname($host); } $coroutine = $scope->spawn(defineTargetIpV4(...), $host); ==== 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(); 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 is a contract that allows objects to be used in the ''await'' expression. 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 ''await '' may produce different results. ==== Await ==== Async\await(Async\Awaitable $awaitable, ?Async\Awaitable $cancellation = null): mixed The ''await'' function is used to wait for the completion of another coroutine or any object that implements the ''Awaitable'' 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. > **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 function Async\timeout; 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() { Async\delay(2000); }); 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(); } ==== 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 ''currentCoroutine()'' 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** and **Scopes** 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. ==== Coroutine Scope ==== > **Coroutine Scope** — the space associated with coroutines created using the ''spawn'' expression. === Motivation === Sometimes it is necessary to gain control not only over a currently running coroutine, but also over all coroutines that will be launched within a new one — without having direct access to them. This could be the case for web server code that handles requests in separate coroutines and does not know how many additional coroutines will be launched, or a ''JobExecutor'' that wants to manage the lifecycle of running jobs. Without such control, the application code loses the ability to resist runtime errors, which increases the risk of a complete service failure. This is why the **Coroutine Scope** pattern is of critical importance in the context of ensuring reliability. The main use cases for ''Scope'' are: - Controlling the lifetime of coroutines created within a single scope (**Point of responsibility**) - Handling errors from all coroutines within the scope - Binding the lifetime of the scope's coroutines to the lifetime of a **PHP object** - Creating a hierarchy of scopes to manage coroutines in a structured way Binding ''Scope'' to objects is a good practice that has proven effective in **Kotlin**. By allowing coroutines to be tied to an object (this could be a ''Screen'' or a ''ViewModel''), it is possible to avoid the error where coroutines outlive the object that manages them. For frameworks, it can be useful to be able to control all coroutines created within a ''Scope'', to apply context-dependent constraints to them. === Scope propagation === By default, all coroutines are associated with the **Global Coroutine Scope**: use function Async\spawn; spawn('file_get_contents', 'file1.txt'); // <- global scope function readFile(string $file): void { return file_get_contents($file); // <- global scope } function mainTask(): void { // <- global scope spawn('readFile', 'file1.txt'); // <- global scope } spawn('mainTask'); // <- global scope If an application never creates **custom Scopes**, its behavior is similar to coroutines in ''Go'': * Coroutines are not explicitly linked to each other. * The lifetime of coroutines is not limited. The method ''Scope::spawn'' creates a **new coroutine** bound to the specified scope. Scope is propagated between coroutines. If a coroutine is launched within a specific Scope, that Scope is considered the current one. The function like ''spawn()'' will create a coroutine within the current Scope. Coroutines created during the execution of this **new coroutine** will become **sibling tasks**: use function Async\spawn; use Async\Scope; $scope = new Scope(); $scope->spawn(function() { // <- new scope echo "Sibling task 1\n"; spawn(function() { // <- $scope is current scope echo "Sibling task 2\n"; spawn(function() { // <- $scope is current scope echo "Sibling task 3\n"; }); }); }); $scope->awaitCompletion(Async\timeout(60000)); **Expected output** Sibling task 1 Sibling task 2 Sibling task 3 **Structure:** main() ← defines a $scope └── $scope = new Scope() ├── task1() ← runs in the $scope ├── task2() ← runs in the $scope ├── task3() ← runs in the $scope Thus, the method ''Scope::spawn'' creates a new branch of sibling coroutines, where the new coroutine exists at the same level as all subsequent ones. The code that owns a ''Scope'' object becomes the **Point of responsibility** for all coroutines executed within that Scope. > A good practice is to ensure that a Scope object has **only ONE owner**. > Passing ''$scope'' as a parameter to other functions or assigning it to multiple objects > is a **potentially dangerous** operation that can lead to complex bugs. > When a ''$scope'' is owned by multiple modules/classes, > there is a risk that they may either accidentally extend the Scope’s lifetime > or accidentally call ''scope::dispose()'' and disrupt its lifecycle. > Because the code is asynchronous, finding the real cause in such cases is much harder than > if the Scope had only a single owner. === Scope waiting === The ''Scope'' class does not implement the ''Awaitable'' interface, and therefore cannot be used in an ''await'' expression. Awaiting a ''Scope'' is a potentially **dangerous operation** that should be performed consciously, not accidentally. A ''Scope'' can own a coroutine that was created “by someone else” “somewhere else.” For example, a programmer explicitly defines a ''$scope'', and then inside a coroutine that belongs to this ''$scope'' calls a function from a third-party library, which in turn uses ''spawn()''. use function Async\spawn; use Async\Scope; $scope = new Scope(); $scope->spawn(function() { // Call to third-party library function thirdPartyFunction(); }); function thirdPartyFunction() { // Library code spawns a coroutine spawn(function() { sleep(10000); // <- long operation }); } When you await a ''Scope'', you do **not know** which coroutines you are actually awaiting. Even if ''$scope'' is used by only one class or module, there is still a chance that someone accidentally created an “incorrect coroutine.” That is why awaiting a ''Scope'' must be bounded by a cancellation token. > To await a group of coroutines, > a specialized primitive should be used, which will be described in a separate ''RFC''. There are several Use-Cases where waiting for a ''Scope'' might be necessary: * Structured concurrency: when a parent awaits the completion of all child coroutines. * Waiting for Scope tasks to complete the cancellation process (see section: [[#cancellation|Cancellation]]). The structured concurrency pattern with waiting for all child coroutines can be useful for applications whose lifetime is explicitly limited by external conditions. For example, the user might stop a console application. To support a task awaiting in a controlled manner, ''Scope'' provides two specific methods: * ''public function awaitCompletion(Awaitable $cancellation): void {}'' * ''public function awaitAfterCancellation(?callable $errorHandler = null, ?Awaitable $cancellation = null): void {}'' The ''awaitCompletion'' method blocks the execution flow until all tasks within the scope are completed. The ''awaitAfterCancellation'' method does the same but is intended to be called only after the scope has been cancelled. use function Async\spawn; use Async\Scope; $scope = new Scope(); $scope->spawn(function() { echo "Sibling task 1\n"; spawn(function() { echo "Sibling task 2\n"; spawn(function() { echo "Sibling task 3\n"; }); }); }); $scope->awaitCompletion(Async\timeout(60000)); **Expected output** Sibling task 1 Sibling task 2 Sibling task 3 The ''Scope'' awaiting methods do not capture any task results, so they cannot be used to await return values. The ''awaitCompletion'' method can only be used with an explicitly defined cancellation token. This requirement helps prevent indefinite waiting. Awaiting the ''$scope'' object also allows handling exceptions from coroutines within the ''$scope'': use Async\Scope; $scope = new Scope(); $scope->spawn(function() { spawn(function() { spawn(function() { throw new Exception("Error occurred"); }); }); }); try { $scope->awaitCompletion(Async\timeout(60000)); } catch (Exception $exception) { echo $exception->getMessage()."\n"; } **Expected output** Error occurred Calling the ''awaitCompletion()'' method after the ''Scope'' has been cancelled will immediately throw a cancellation exception. use Async\Scope; $scope = new Scope(); try { $scope->spawn(task1(...)); $scope->spawn(task2(...)); $scope->cancel(); // Wait all tasks $scope->awaitCompletion(Async\timeout(60000)); } catch (Exception $exception) { echo "Caught exception: ",$exception->getMessage()."\n"; } **Expected output** Caught exception: cancelled at ... In this example, ''$scope->awaitCompletion(Async\timeout(60000));'' will immediately throw an exception. If you need to wait for the ''Scope'' to complete after it has been cancelled, use the special method ''awaitAfterCancellation'', which is designed for this case. use function Async\spawn; use Async\Scope; $scope = new Scope(); spawn(function() { try { $scope->awaitCompletion(Async\timeout(60000)); } catch (\Async\CancellationError $exception) { $scope->awaitAfterCancellation(); echo "Caught exception: ",$exception->getMessage()."\n"; } }); $scope->spawn(function() use ($scope) { $scope->cancel(); try { Async\delay(1000); } finally { sleep(1); echo "Finally\n"; } }); **Expected output** Finally Caught exception: cancelled at ... In this example, the line ''Finally'' will be printed first because ''$scope->awaitAfterCancellation()'' waits for all coroutines inside the Scope to complete. The ''awaitAfterCancellation'' method is used in scenarios where final resource cleanup is required after all child tasks are guaranteed to have finished execution. There is also a risk of indefinite waiting, so it is **recommended** to explicitly specify a timeout. === Scope Hierarchy === A hierarchy can be a convenient way to describe an application as a set of dependent tasks: * Parent tasks are connected to child tasks and are responsible for their execution time. * Tasks on the same hierarchy level are independent of each other. * Parent tasks should control their child's tasks. * Child tasks MUST NOT control or wait for their parent tasks. * It is correct if tasks at the same hierarchy level are only connected to tasks of the immediate child level. WebServer ├── Request Worker │ ├── request1 task │ │ ├── request1 subtask A │ │ └── request1 subtask B │ └── request2 task │ ├── request2 subtask A │ └── request2 subtask B The work of a web server can be represented as a hierarchy of task groups that are interconnected. The ''Request Worker'' is a task responsible for handling incoming requests. There can be multiple requests. Each request may spawn subtasks. On the same level, all requests form a group of request-tasks. ''Scope'' is fit for implementing this concept: WebServer ├── Request Worker │ ├── request1 Scope │ │ ├── request1 subtask A │ │ │ └── subtask A Scope │ │ │ ├── sub-subtask A1 │ │ │ └── sub-subtask A2 │ │ └── request1 subtask B │ └── request2 Scope │ ├── request2 subtask A │ └── request2 subtask B │ └── subtask B Scope │ └── sub-subtask B1 The ''new Scope()'' constructor creates a **root independent Scope**. To create a **child Scope**, you need to use a special constructor: ''Scope::inherit(?Scope $parentScope = null)'' It returns a new ''Scope'' object that acts as a child. A coroutine created within the child ''Scope'' can also be considered a child relative to the coroutines in the parent ''Scope''. If the ''$parentScope'' parameter is not specified, the current ''Scope'' will be used as the parent. **An example:** use function Async\spawn; use Async\Scope; use Async\CancellationError; // New independent root scope $scope = new Scope(); // child scope from $scope number 1 $childScope1 = Scope::inherit($scope); $scope->spawn(function() { $childScope2 = Scope::inherit(); // child scope from $scope number 2 }); **WebServer example:** use function Async\await; use Async\Scope; use Async\CancellationError; function webServer(): void { // Creating a new Root Scope $scope = new Scope(); // socket server code that accepts connections... while ($socket = acceptConnection()) { $scope->spawn(connectionHandler(...), $socket); } } function connectionHandler($socket): void { // Current scope is the Root Scope // $scope is inherited from the current Scope. $scope = Scope::inherit(); // We do not provide direct access to the Scope object in other functions $cancelToken = fn(string $message) => $scope->cancel(new CancellationError($message)); $scope->spawn(function() use ($socket, $cancelToken) { $limiterScope = Scope::inherit(); // child scope for connectionLimiter $limiterScope->spawn(function() { Async\delay(10000); $cancelToken("The request processing limit has been reached."); }, $cancelToken); handleRequest($socket, $cancelToken); }); } Let's examine how this example works. - The ''webServer'' function creates a new ''Root Scope'' for the entire server. - Each time a new connection is accepted, a new coroutine is created within the ''Root Scope''. - ''connectionHandler'' creates an inherited ''Scope'' from the current context. - A ''$cancelToken'' closure is created to provide a simple way to cancel the entire scope with a custom message. - A child ''$limiterScope'' is created for timeout management. - A timeout coroutine automatically cancels the request after 10 seconds with the message "The request processing limit has been reached." Root <- Root Scope │ ├── connectionHandler (Scope) <- inherited scope │ │ │ ├── main request handling (Coroutine) <- $scope │ │ └── timeout limiter (Coroutine) <- $limiterScope │ │ The ''connectionHandler'' uses structured concurrency principles where the timeout limiter coroutine is automatically managed within its dedicated ''$limiterScope''. When the request processing completes or times out, all associated coroutines are properly cleaned up through the scope hierarchy. ''$limiterScope'' is used to isolate the timeout management logic from the main request handling that should be cancelled when the request is completed. === Scope cancellation === The ''cancel'' method cancels all child coroutines and all child ''Scopes'' of the current ''Scope''.: use function Async\spawn; use Async\Scope; echo "Start\n"; $scope = new Scope(); $scope->spawn(function() { spawn(function() { Async\delay(1000); echo "Task 1\n"; }); spawn(function() { Async\delay(2000); echo "Task 2\n"; }); }); $scope->cancel(); echo "End\n"; **Expected output** Start End === Scope disposal === **Coroutine Scope** has several resource cleanup strategies that can be triggered either explicitly, on demand, or implicitly when the ''Scope'' object loses its last reference. There are three available strategies for ''Scope'' termination: | **Method** | | | ''disposeSafely'' | Marks as zombie coroutines, does not cancel | | ''dispose'' | Cancels with a warning | | ''disposeAfterTimeout'' | Issues a warning, then cancels after a delay | The main goal of all three methods is to terminate the execution of coroutines that belong to the ''Scope'' or its child Scopes. However, each method approaches this task slightly differently. The ''disposeSafely'' method is used by default in the destructor of the ''Async\Scope'' class. Its key feature is transitioning coroutines into a **zombie coroutine** state. A **zombie coroutine** continues execution but is tracked by the system differently than regular coroutines. (See section: [[#zombie_coroutine_policy|Zombie coroutine policy]]). A warning is issued when a **zombie coroutine** is detected. use function Async\spawn; use function Async\await; use Async\Scope; $scope = new Scope(); await($scope->spawn(function() { spawn(function() { Async\delay(1000); echo "Task 1\n"; }); spawn(function() { Async\delay(2000); echo "Task 2\n"; }); echo "Root task\n"; })); $scope->disposeSafely(); **Expected output** Root task Warning: Coroutine is zombie at ... in Scope disposed at ... Warning: Coroutine is zombie at ... in Scope disposed at ... Task 1 Task 2 The ''$scope'' variable is released immediately after the coroutine ''Root task'' completes execution, so the child coroutine ''Task 1'' does not have time to execute before the ''disposeSafely'' method is called. ''disposeSafely'' detects this and signals it with a warning but allows the coroutine to complete. The ''Scope::dispose'' method differs from ''Scope::disposeSafely'' in that it does not leave **zombie coroutines**. It cancels **all coroutines**. When coroutines are detected as unfinished, a warning is issued. **Example** use function Async\spawn; use function Async\await; use Async\Scope; $scope = new Scope(); await($scope->spawn(function() { spawn(function() { Async\delay(1000); echo "Task 1\n"; }); spawn(function() { Async\delay(2000); echo "Task 2\n"; }); echo "Root task\n"; })); $scope->dispose(); **Expected output** Warning: Coroutine is zombie at ... in Scope disposed at ... Warning: Coroutine is zombie at ... in Scope disposed at ... Warning: Coroutine is zombie at ... in Scope disposed at ... The ''disposeAfterTimeout'' method is a delayed version of the ''disposeSafely'' method. The ''$timeout'' parameter must be greater than zero but less than 10 minutes. use Async\Scope; class Service { private Scope $scope; public function __construct() { $this->scope = new Scope(); } public function __destruct() { $this->scope->disposeAfterTimeout(5000); } public function run(): void { $this->scope->spawn(function() { spawn(function() { Async\delay(1000); echo "Task 2\n"; Async\delay(5000); echo "Task 2 next line never executed\n"; }); echo "Task 1\n"; }); } } $service = new Service(); $service->run(); sleep(1); unset($service); **Expected output** Task 1 Warning: Coroutine is zombie at ... in Scope disposed at ... Task 2 The ''dispose*()'' methods can be called multiple times, which is not considered an error. If the ''Scope::cancel()'' method is called with a parameter after the ''Scope'' has already been cancelled, **PHP** will emit a warning indicating that the call will be ignored. === Scope cancellation/disposal order === If a ''Scope'' has child ''Scopes'', the coroutines in the child ''Scopes'' will be canceled or disposed first, followed by those in the parent — from the bottom up in the hierarchy. This approach increases the likelihood that resources will be released correctly. However, it does not guarantee this, since the exact order of coroutines in the execution queue cannot be determined with 100% certainty. During the release of child ''Scopes'', the same cleanup strategy is used that was applied to the parent ''Scope''. If the ''disposeSafely'' method is called, the child Scopes will also be released using the ''disposeSafely'' strategy. If the ''dispose'' method is used, the child Scopes will use the same method for cleanup. The ''disposeAfterTimeout'' method will delay the execution of ''disposeSafely'' for the specified time. === Spawn with disposed scope === When the ''cancel()'' or ''dispose()'' method is called, the ''Scope'' is marked as closed. Attempting to launch a coroutine with this Scope will result in a fatal exception. use Async\Scope; $scope = new Scope(); $scope->spawn(function() { echo "Task 1\n"; }); $scope->cancel(); $scope->spawn(function() { // <- AsyncException: Coroutine scope is closed echo "Task 2\n"; }); ==== Error detection ==== Detecting erroneous situations when using coroutines is an important part of analyzing an application's reliability. **The following scenarios are considered potentially erroneous** - A coroutine belongs to a global scope and is not awaited by anyone (a **zombie coroutine**). - The root scope has been destroyed (its destructor was called), but no one awaited it or ensured that its resources were explicitly cleaned up (e.g., by calling ''$scope->cancel()'' or ''$scope->dispose()''). - An attempt to await a coroutine from within itself. - Awaiting ''$scope'' from within itself or from one of its child scopes. - Stuck tasks in the cancellation state. - Deadlocks caused by circular dependencies between coroutines. **PHP** will respond to such situations by issuing **warnings**, including debug information about the involved coroutines. Developers are expected to write code in a way that avoids triggering these warnings. An attempt to use the expression ''await($coroutine)'' from within the same coroutine throws an exception. use function Async\spawn; use function Async\await; $coroutine = null; $coroutine = spawn(function() use (&$coroutine) { await($coroutine); // <- AsyncException: A coroutine cannot await itself. Coroutine spawned at ... }); Using the ''Scope::awaitCompletion()'' from a coroutine that belongs to the same ''$scope'' or to one of its child scopes will throw a fatal exception. This condition makes it impossible to perform the ''$globalScope->awaitCompletion'' call. use Async\Scope; $scope = new Scope(); $scope->spawn(function() use ($scope) { $scope->awaitCompletion(Async\timeout(1000)); // <- AsyncException: Awaiting a scope from within itself or // its child scope would cause a deadlock. Scope created at ... }); === Error mitigation strategies === To avoid accidentally hanging coroutines whose lifetimes were not correctly limited, follow these rules: * Use **separate Scopes** for different coroutines. This is the best practice, as it allows explicitly defining lifetime dependencies between Scopes. * Use ''Scope::dispose()''. The ''dispose()'' method cancels coroutine execution and logs an error. * Don't mix semantically different coroutines within the same ''Scope''. * Avoid building hierarchies between ''Scopes'' with complex interdependencies. * Do not use cyclic dependencies between ''Scopes''. * The principle of single point of responsibility and ''Scope'' ownership. Do not pass the ''Scope'' object to different coroutine functions (unless the action happens in a closure). Do not store ''Scope'' objects in different places. Violating this rule can lead to manipulations with ''Scope'', which may cause a deadlock or disrupt the application's logic. * Child coroutines should not wait for their parents. Child Scopes should not wait for their parents. namespace ProcessPool; use Async\Scope; final class ProcessPool { private Scope $watcherScope; private Scope $jobsScope; private Scope $pool; /** * List of pipes for each process. * @var array */ private array $pipes = []; /** * Map of process descriptors: pid => bool * If the value is true, the process is free. * @var array */ private array $descriptors = []; public function __construct(readonly public string $entryPoint, readonly public int $max, readonly public int $min) { // Define the coroutine scopes for the pool, watcher, and jobs $this->watcherScope = new Scope(); $this->jobsScope = new Scope(); $this->pool = new Scope(); } public function __destruct() { $this->watcherScope->dispose(); $this->pool->dispose(); $this->jobsScope->dispose(); } public function start(): void { $this->watcherScope->spawn($this->processWatcher(...)); for ($i = 0; $i < $this->min; $i++) { $this->pool->spawn($this->startProcess(...)); } } public function stop(): void { $this->watcherScope->cancel(); $this->pool->cancel(); $this->jobsScope->cancel(); } private function processWatcher(): void { while (true) { try { $this->pool->awaitCompletion(Async\timeout(60000)); } catch (StopProcessException $exception) { echo "Process was stopped with message: {$exception->getMessage()}\n"; if($exception->getCode() !== 0 || count($this->descriptors) < $this->min) { $this->pool->spawn($this->startProcess(...)); } } } } } The example above demonstrates how splitting coroutines into Scopes helps manage their interaction and reduces the likelihood of errors. Here, ''watcherScope'' monitors tasks in ''pool''. When a process finishes, the watcher detects this event and, if necessary, starts a new process or not. The monitoring logic is completely separated from the process startup logic. The lifetime of ''watcherScope'' matches that of ''pool'', but not longer than the lifetime of the watcher itself. The overall lifetime of all coroutines in the ''ProcessPool'' is determined by the lifetime of the ''ProcessPool'' object or by the moment the ''stop()'' method is explicitly called. ==== Zombie coroutine policy ==== Coroutines whose lifetime extends beyond the boundaries of their parent ''Scope'' are handled according to a separate **policy**. This policy aims to strike a balance between uncontrolled resource leaks and the need to abruptly terminate coroutines, which could lead to data integrity violations. If there are no active coroutines left in the execution queue and no events to wait for, the application is considered complete. **Zombie coroutines** differ from regular ones in that they are not counted as active. Once the application is considered finished, zombie coroutines are given a time limit within which they must complete execution. If this limit is exceeded, all zombie coroutines are canceled. The delay time for handling zombie coroutines can be configured using a constant in the ''php.ini'' file: ''async.zombie_coroutine_timeout'', which is set to two seconds by default. If a coroutine is created within a user-defined ''Scope'', the programmer can set a custom timeout for that specific ''Scope'' using the ''Scope::disposeAfterTimeout(int $ms)'' method. === Structured concurrency === **Structured concurrency** allows organizing coroutines into a group or hierarchy to manage their lifetime or exception handling. The parent task is required to take responsibility for its child tasks and must not complete before the children have finished their execution. To implement structured concurrency, it is recommended to use the ''Scope'' class with proper hierarchy management. The following code implements this idea: use Async\Scope; function copyFile(string $sourceFile, string $targetFile): void { $source = fopen($sourceFile, 'r'); $target = fopen($targetFile, 'w'); $buffer = null; try { // Child scope $tasks = new \Async\CoroutineGroup(Scope::inherit()); // Read data from the source file $tasks->spawn(function() use (&$buffer, $source) { while (!feof($source)) { if ($buffer === null) { $chunk = fread($source, 1024); $buffer = $chunk !== false && $chunk !== '' ? $chunk : null; } suspend(); } $buffer = ''; }); // Write data to the target file $tasks->spawn(function() use (&$buffer, $target) { while (true) { if (is_string($buffer)) { if ($buffer === '') { break; // End of a file } fwrite($target, $buffer); $buffer = null; } suspend(); } echo "Copy complete.\n"; }); await($tasks); } finally { fclose($source); fclose($target); } } $copyTasks = new \Async\Scope(); $copyTasks->spawn(copyFile(...), 'source.txt', 'target.txt'); $copyTasks->spawn(copyFile(...), 'source2.txt', 'target2.txt'); $copyTasks->awaitCompletion(Async\timeout(60000)); The example creates two task groups: a parent and a child. The parent task group handles the copy operations directly, while the child tasks perform file reading and writing. File descriptors will not be closed until all child copy tasks have completed. The main code will not finish until all copy operations are completed. ==== Timer functions ==== The standard async library includes two functions similar to ''usleep()'': * ''Async\delay(int $ms): void'' * ''Async\timeout(int $ms): Awaitable'' The ''delay'' function suspends the execution of a coroutine for the specified number of milliseconds. Unlike ''usleep'', the ''delay'' function will throw a cancellation exception if the coroutine is cancelled. The ''timeout'' function is similar to ''delay'', but it returns an ''Awaitable'' object: use function Async\spawn; use function Async\await; use function Async\timeout; use function Async\delay; try { delay(1000); // suspends the coroutine for 1 second // Try to fetch data from the URL within 1 second echo await(spawn('file_get_content', 'https://php.net/'), timeout(1000)); } catch (\Async\AwaitCancelledException) { echo "Operation was cancelled by timeout\n"; } catch (\Async\CancellationError) { echo "Operation was cancelled by user\n"; } ==== Error Handling ==== An uncaught exception in a coroutine follows this flow: - If the coroutine is awaited using the ''await'' keyword, the exception is propagated to the awaiting points. If multiple points are awaiting, each will receive the same exception (**Each await point will receive the exact same exception object, not cloned**). - The exception is passed to the ''Scope''. - If the ''Scope'' has an exception handler defined, it will be invoked. - If the ''Scope'' does not have an exception handler, the ''cancel()'' method is called, canceling all coroutines in this scope, including all child scopes. - If the ''Scope'' has responsibility points, i.e., the construction ''Scope::awaitCompletion'', all responsibility points receive the exception. - Otherwise, the exception is passed to the parent scope if it is defined. - If there is no parent scope, the exception falls into ''globalScope'', where the same rules apply as for a regular scope. > **Note:** In addition to regular exception handling, when a deadlock condition is detected > (circular dependencies between coroutines), a ''DeadlockError'' is thrown as a fatal error > at the end of the application lifecycle. This error is not intended for normal exception handling > but serves as a diagnostic tool to identify architectural problems. {{ :rfc:true_async:exception_flow.svg |}} **Example** use Async\Scope; $scope = new Scope(); $scope->spawn(function() { throw new Exception("Task 1"); }); $exception1 = null; $exception2 = null; $scope2 = new Scope(); $scope2->spawn(function() use ($scope, &$exception1) { try { $scope->awaitCompletion(Async\timeout(60000)); } catch (Exception $e) { $exception1 = $e; echo "Caught exception1: {$e->getMessage()}\n"; } }); $scope2->spawn(function() use ($scope, &$exception2) { try { $scope->awaitCompletion(Async\timeout(60000)); } catch (Exception $e) { $exception2 = $e; echo "Caught exception2: {$e->getMessage()}\n"; } }); $scope2->awaitCompletion(Async\timeout(60000)); echo $exception1 === $exception2 ? "The same exception\n" : "Different exceptions\n"; If an exception reaches ''globalScope'' and is not handled in any way, it triggers **Graceful Shutdown Mode**, which will terminate the entire application. The ''Scope'' class allows defining an exception handler that can prevent exception propagation. For this purpose, two methods are used: - **''setExceptionHandler''** – triggers for any exceptions thrown within this **Scope**. - **''setChildScopeExceptionHandler''** – triggers for exceptions from **child Scopes**. > The methods ''setExceptionHandler'' and ''setChildScopeExceptionHandler'' cannot be used with the ''globalScope''. > If an attempt is made to do so, an exception will be thrown. **Example** $scope = new Scope(); $scope->setExceptionHandler(function (Async\Scope $scope, Async\Coroutine $coroutine, Throwable $e) { echo "Caught exception: {$e->getMessage()}\n in coroutine: {$coroutine->getSpawnLocation()}\n"; }); $scope->spawn(function() { throw new Exception("Task 1"); }); $scope->awaitCompletion(Async\timeout(60000)); Using these handlers, you can implement the **Supervisor** pattern, i.e., a **Scope** that will not be canceled when an exception occurs in coroutines. > If the ''setExceptionHandler'' or ''setChildScopeExceptionHandler'' handlers throw an exception, > it will be propagated to the **parent Scope** or the **global Scope**. The **''setChildScopeExceptionHandler''** method allows handling exceptions only from **child Scopes**, which can be useful for implementing an algorithm where the **main Scope** runs core tasks, while **child Scopes** handle additional ones. For example: use Async\Scope; use Async\Coroutine; final class Service { private Scope $scope; public function __construct() { $this->scope = new Scope(); $this->scope->setChildScopeExceptionHandler( static function (Scope $scope, Coroutine $coroutine, \Throwable $exception): void { echo "Occurred an exception: {$exception->getMessage()} in Coroutine {$coroutine->getSpawnLocation()}\n"; }); } public function start(): void { $this->scope->spawn($this->run(...)); } public function stop(): void { $this->scope->cancel(); } private function run(): void { while (($socket = $this->service->receive()) !== null) { $scope = Scope::inherit($this->scope); // supervisor pattern ($scope->spawn($this->handleRequest(...), $socket)->onFinally( static function () use ($scope) { $scope->disposeSafely(); } ); } } } ''$this->scope'' listens for new connections on the server socket. Canceling ''$this->scope'' means shutting down the entire service. Each new connection is handled in a separate **Scope**, which is inherited from ''$this->scope''. If an exception occurs in a coroutine created within a **child Scope**, it will be passed to the ''setChildScopeExceptionHandler'' handler and will not affect the operation of the service as a whole. {{ :rfc:true_async:supervisor.svg |}} === Responsibility points === A **responsibility point** is code that explicitly waits for the completion of a coroutine or a ''Scope'': $scope = new Scope(); $scope->spawn(function() { throw new Exception("Task 1"); }); try { $scope->awaitCompletion(Async\timeout(60000)); } catch (\Throwable $e) { echo "Caught exception: {$e->getMessage()}\n"; } === Exception Handling === The ''Scope'' class provides a method for handling exceptions: use Async\Scope; $scope = new Scope(); $scope->spawn(function() { throw new Exception("Task 1"); }); $scope->setExceptionHandler(function (Exception $e) { echo "Caught exception: {$e->getMessage()}\n"; }); $scope->awaitCompletion(Async\timeout(60000)); An exception handler has the right to suppress the exception. However, if the exception handler throws another exception, the exception propagation algorithm will continue. ==== onFinally ==== The ''onFinally'' method allows defining a callback function that will be invoked when a coroutine or scope completes. This method can be considered a direct analog of ''defer'' in Go. > ⚠️ **Important:** All ''onFinally'' 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. === Scope::onFinally === For ''Scope'', the callback receives the completed scope as a parameter: use Async\Scope; $scope = new Scope(); $scope->spawn(function() { throw new Exception("Task 1"); }); $scope->onFinally(function (Scope $completedScope) { echo "Scope " . spl_object_id($completedScope) . " completed\n"; }); $scope->awaitCompletion(Async\timeout(60000)); === Coroutine::onFinally === 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->onFinally(function (Coroutine $completedCoroutine) { echo "Coroutine " . spl_object_id($completedCoroutine) . " completed\n"; }); The ''onFinally'' 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'); onFinally(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 ''CancellationError'' 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 and scopes using the ''cancel()'' method: function task(): void {} $coroutine = spawn(task(...)); // cancel the coroutine $coroutine->cancel(new Async\CancellationError('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 ''CancellationError'', 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 ''CancellationError''. > ⚠️ **Warning:** You should not attempt to suppress ''CancellationError'' exception, > as it may cause application malfunctions. use Async\Scope; $scope = new Scope(); $scope->spawn(function() { sleep(1); echo "Task 1\n"; }); $scope->cancel(new Async\CancellationError('Task was cancelled')); Canceling a ''Scope'' triggers the cancellation of all coroutines within that ''Scope'' and all child ''Scopes'' in hierarchical order. > > **Note:** ''CancellationError'' can be extended by the user > to add metadata that can be used for debugging purposes. > ==== 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 ''CancellationError'' as its final result: use function Async\spawn; $coroutine = spawn(function() use (&$coroutine) { $coroutine->cancel(new \Async\CancellationError("Self-cancelled")); echo "This still executes\n"; // Will execute return "completed"; }); // await() will throw CancellationError despite normal completion A coroutine can also cancel its own ''Scope'', which **does** affect execution by cancelling all coroutines in the scope, including itself: use Async\Scope; $scope = new Scope(); $scope->spawn(function() use ($scope) { $scope->cancel(new \Async\CancellationError("Scope cancelled")); echo "This executes\n"; }); ==== CancellationError handling ==== In the context of coroutines, it is not recommended to use ''catch \Throwable'' or ''catch CancellationError''. Since ''CancellationError'' 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() { 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() { sleep(1); throw new \Exception("Task 1"); }); spawn(function() use ($coroutine) { $coroutine->cancel(); }); try { await($coroutine); } catch (Async\CancellationError $exception) { // not recommended way to handle exceptions echo "Caught CancellationError\n"; throw $exception; } } finally { echo "The end\n"; } Expected output Caught CancellationError The end === CancellationError propagation === The ''CancellationError'' 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 ''CancellationError'' 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). ==== Critical section ==== Sometimes it's necessary to execute a **critical section** of code that must not be cancelled via ''CancellationError''. For example, this could be a sequence of write operations or a transaction. For this purpose, the ''Async\protect'' function is used, which allows executing a closure in a non-cancellable (silent) mode. use function Async\spawn; function task(): void { Async\protect(fn() => fwrite($file, "Critical data\n")); } spawn(task(...)); If a ''CancellationError'' was sent to a coroutine during ''protect()'', the exception will be thrown immediately after the execution of ''protect()'' completes. The use of loops or unsafe operations inside a critical section can be checked by static analyzers. ==== 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. === exit and die keywords === The ''exit''/''die'' keywords called within a coroutine result in the immediate termination of the application. Unlike the ''cancel()'' operation, they do not allow for proper resource cleanup. ==== 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 in ''globalScope'', 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 beyond the scope of this **RFC**. The **Graceful Shutdown** mode can also be triggered using the function: Async\gracefulShutdown(?CancellationError $CancellationError = null): void {} from anywhere in the application. === 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 **DeadlockError** exception is thrown as a fatal error. This exception extends **Error** and indicates a critical architectural problem in the application design. The **DeadlockError** 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\DeadlockError: // 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\Scope::getChildScopes()'' method returns an array of all child scopes of the current scope. The method ''Async\Scope::getCoroutines()'' returns a list of coroutines that are registered within the specified ''Scope''. The ''Async\getCoroutines()'' method returns an array of all coroutines in the application. ===== Backward Incompatible Changes ===== Simultaneous use of the **True Async API** and the **Fiber API** is not possible, as Fibers and coroutines manage the same resources (execution context, stacks) in different ways: ^ API ^ Execution model ^ API level ^ | **Fibers** | Symmetric execution contexts | Low-level API where the programmer explicitly controls switching | | **TrueAsync** | Switches contexts in an arbitrary order | High-level API where the programmer does not manage the switching | Thus, these two APIs are incompatible not only at the technical level but also at the logical level. If you try to create a ''Fiber'' inside a coroutine, you will get a ''FiberError'': use function Async\spawn; spawn(function() { // This will throw FiberError $fiber = new Fiber(function() { echo "This won't work\n"; }); }); Error message: "Cannot create a fiber while an True Async is active" - If ''new Fiber()'' is called first, subsequent ''Async\spawn'' calls will fail - If ''Async\spawn'' is called first, any attempt to create a **Fiber** throws ''FiberError'' ===== Proposed PHP Version(s) ===== PHP 8.6+ ===== RFC Impact ===== ==== To SAPIs ==== The **True Async** module activates the reactor within the context of ''php_request_startup\php_request_shutdown()'' request processing. Therefore, using concurrency is reasonable only for long-life scenarios implemented via CLI. It is expected that **True Async** will enable the integration of built-in **web servers** into PHP, which will be embedded into the reactor’s event loop. === NGINX Unit integration example === [[https://github.com/EdmondDantes/nginx-unit/tree/true-async|NGINX Unit TrueAsync Integration]] demonstrates a web server integration using **True Async**. ''NGINX Unit'' with **TrueAsync** enables handling multiple ''HTTP'' requests within a single process through ''PHP'' coroutines. Each incoming ''HTTP'' request spawns a dedicated coroutine, enabling concurrent request processing without creating new processes or threads. Instead of traditional ''PHP'' globals (''$_SERVER'', ''$_POST''), provides the ''NginxUnit\'' namespace with ''Request'' and ''Response'' classes. Request handlers are registered via ''HttpServer::onRequest()''. * **Entrypoint Loading**: The entrypoint ''PHP'' file is loaded **once** at process startup (not per request), registering the callback in the global variable ''nxt_php_request_callback''. * **Coroutine Lifecycle**: Coroutines suspend and resume through coordination with TrueAsync's event loop, enabling true non-blocking operation. * **Request Flow**: ''HTTP Request'' → ''nxt_php_request_handler()'' → ''ZEND_ASYNC_SPAWN()'' → ''nxt_php_request_coroutine_entry()'' → User callback → ''response->write()'' → ''nxt_unit_response_write_nb()'' → Event loop → Response completion. Example of entrypoint: [[https://github.com/EdmondDantes/nginx-unit/blob/true-async/src/true-async-php/entrypoint.php|entrypoint.php]] Git: https://github.com/EdmondDantes/nginx-unit/tree/true-async ==== 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 - **CancellationError suppression detection:** ''catch (\Throwable)'' that may hide CancellationError **Libraries Using Fiber:** Libraries that use the ''Fiber API'' will continue to work as expected, but cannot be used simultaneously with ''True Async''. ==== php.ini Defaults ==== * async.zombie_coroutine_timeout - default 5 seconds ===== Open Issues ===== None. ===== Future Scope ===== === Reactor === At the moment, the Reactor component uses the ''LibUV'' library. This component should be implemented as a separate extension. ===== Proposed Voting Choices ===== Yes or no vote. 2/3 required to pass. * Yes * No ===== 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** - * **TrueAsync API RFC** - https://github.com/true-async/php-true-async-rfc/blob/main/true-async-api-rfc.md 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.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.