rfc:fibers
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
rfc:fibers [2021/02/12 18:00] – Update for simpler API trowski | rfc:fibers [2021/07/12 21:30] (current) – kelunik | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== PHP RFC: Fibers ====== | ====== PHP RFC: Fibers ====== | ||
- | * Date: 2021-01-06 | + | * Date: 2021-03-08 |
* Authors: Aaron Piotrowski < | * Authors: Aaron Piotrowski < | ||
- | * Status: | + | * Status: |
* First Published at: http:// | * First Published at: http:// | ||
===== Introduction ===== | ===== Introduction ===== | ||
- | For most of PHP’s history, people have written PHP code only as synchronous code. They have code that runs synchronously and in turn, only calls functions | + | For most of PHP’s history, people have written PHP code only as synchronous code. Execution of functions |
- | More recently, there have been multiple projects that have allowed people to write asynchronous PHP code. Asynchronous functions accept a callback or return a placeholder for a future value (such as a promise) to run code at a future time once the result is available. Execution continues without waiting for a result. Examples of these projects are [[https:// | + | More recently, there have been multiple projects that have allowed people to write asynchronous PHP code to allow for concurrent I/O operations. Asynchronous functions accept a callback or return a placeholder for a future value (such as a promise) to run code at a future time once the result is available. Execution continues without waiting for a result. Examples of these projects are [[https:// |
The problem this RFC seeks to address is a difficult one to explain, but can be referred to as the [[https:// | The problem this RFC seeks to address is a difficult one to explain, but can be referred to as the [[https:// | ||
Line 18: | Line 18: | ||
* Asynchronous functions change the way the function must be called. | * Asynchronous functions change the way the function must be called. | ||
* Synchronous functions may not call an asynchronous function (though asynchronous functions may call synchronous functions). | * Synchronous functions may not call an asynchronous function (though asynchronous functions may call synchronous functions). | ||
- | * Calling an asynchronous function requires the entire | + | * Calling an asynchronous function requires the entire |
- | For people who are familiar with using promises and await/yield to achieve writing asynchronous code, the problem can be expressed as: “Once one function returns a promise somewhere in your call stack, the entire call stack needs to return a promise because the result of the call cannot be known until the promise is resolved.” | + | For people who are familiar with using promises and/or await/yield to achieve writing asynchronous code, the problem can be expressed as: “Once one function returns a promise somewhere in your call stack, the entire call stack needs to return a promise because the result of the call cannot be known until the promise is resolved.” |
This RFC seeks to eliminate the distinction between synchronous and asynchronous functions by allowing functions to be interruptible without polluting the entire call stack. This would be achieved by: | This RFC seeks to eliminate the distinction between synchronous and asynchronous functions by allowing functions to be interruptible without polluting the entire call stack. This would be achieved by: | ||
* Adding support for [[https:// | * Adding support for [[https:// | ||
- | * Adding '' | + | * Adding |
* Adding exception classes '' | * Adding exception classes '' | ||
+ | |||
+ | Fibers allow for transparent non-blocking I/O implementations of existing interfaces (such as PSR-7, Doctine ORM, etc.). This is because the placeholder (promise) object is eliminated. Functions instead can declare the I/O result type instead of a placeholder object which cannot specify a resolution type because PHP does not support generics. | ||
==== Fibers ==== | ==== Fibers ==== | ||
Line 47: | Line 49: | ||
A Fiber would be represented as class which would be defined in core PHP with the following signature: | A Fiber would be represented as class which would be defined in core PHP with the following signature: | ||
+ | |||
+ | < | ||
<code php> | <code php> | ||
Line 54: | Line 58: | ||
* @param callable $callback Function to invoke when starting the fiber. | * @param callable $callback Function to invoke when starting the fiber. | ||
*/ | */ | ||
- | public function __construct(callable $callback) { } | + | public function __construct(callable $callback) {} |
/** | /** | ||
Line 61: | Line 65: | ||
* @param mixed ...$args Arguments passed to fiber function. | * @param mixed ...$args Arguments passed to fiber function. | ||
* | * | ||
- | * @return mixed Value from the first suspension point. | + | * @return mixed Value from the first suspension point or NULL if the fiber returns. |
* | * | ||
- | * @throw FiberError If the fiber is running or terminated. | + | * @throw FiberError If the fiber has already been started. |
* @throw Throwable If the fiber callable throws an uncaught exception. | * @throw Throwable If the fiber callable throws an uncaught exception. | ||
*/ | */ | ||
- | public function start(mixed ...$args): mixed { } | + | public function start(mixed ...$args): mixed {} |
/** | /** | ||
Line 74: | Line 78: | ||
* @param mixed $value | * @param mixed $value | ||
* | * | ||
- | * @return mixed Value from the next suspension point or NULL if the fiber terminates. | + | * @return mixed Value from the next suspension point or NULL if the fiber returns. |
* | * | ||
- | * @throw FiberError If the fiber is running or terminated. | + | * @throw FiberError If the fiber has not started, |
* @throw Throwable If the fiber callable throws an uncaught exception. | * @throw Throwable If the fiber callable throws an uncaught exception. | ||
*/ | */ | ||
- | public function resume(mixed $value = null): mixed { } | + | public function resume(mixed $value = null): mixed {} |
/** | /** | ||
Line 87: | Line 91: | ||
* @param Throwable $exception | * @param Throwable $exception | ||
* | * | ||
- | * @return mixed Value from the next suspension point or NULL if the fiber terminates. | + | * @return mixed Value from the next suspension point or NULL if the fiber returns. |
* | * | ||
- | * @throw FiberError If the fiber is running or terminated. | + | * @throw FiberError If the fiber has not started, |
* @throw Throwable If the fiber callable throws an uncaught exception. | * @throw Throwable If the fiber callable throws an uncaught exception. | ||
*/ | */ | ||
- | public function throw(Throwable $exception): | + | public function throw(Throwable $exception): |
/** | /** | ||
* @return bool True if the fiber has been started. | * @return bool True if the fiber has been started. | ||
*/ | */ | ||
- | public function isStarted(): | + | public function isStarted(): |
/** | /** | ||
* @return bool True if the fiber is suspended. | * @return bool True if the fiber is suspended. | ||
*/ | */ | ||
- | public function isSuspended(): | + | public function isSuspended(): |
/** | /** | ||
* @return bool True if the fiber is currently running. | * @return bool True if the fiber is currently running. | ||
*/ | */ | ||
- | public function isRunning(): | + | public function isRunning(): |
/** | /** | ||
- | * @return bool True if the fiber has completed execution. | + | * @return bool True if the fiber has completed execution |
*/ | */ | ||
- | public function isTerminated(): | + | public function isTerminated(): |
/** | /** | ||
- | * @return mixed Return value of the fiber callback. | + | * @return mixed Return value of the fiber callback. NULL is returned if the fiber does not have a return statement. |
* | * | ||
- | * @throws FiberError If the fiber has not terminated or did not return a value. | + | * @throws FiberError If the fiber has not terminated or the fiber threw an exception. |
*/ | */ | ||
- | public function getReturn(): | + | public function getReturn(): |
/** | /** | ||
* @return self|null Returns the currently executing fiber instance or NULL if in {main}. | * @return self|null Returns the currently executing fiber instance or NULL if in {main}. | ||
*/ | */ | ||
- | public static function this(): ?self { } | + | public static function this(): ?self {} |
/** | /** | ||
Line 135: | Line 139: | ||
* @return mixed Value provided to {@see Fiber:: | * @return mixed Value provided to {@see Fiber:: | ||
* | * | ||
+ | * @throws FiberError Thrown if not within a fiber (i.e., if called from {main}). | ||
* @throws Throwable Exception provided to {@see Fiber:: | * @throws Throwable Exception provided to {@see Fiber:: | ||
*/ | */ | ||
- | public static function suspend(mixed $value = null): mixed { } | + | public static function suspend(mixed $value = null): mixed {} |
} | } | ||
</ | </ | ||
- | A '' | + | A '' |
+ | |||
+ | '' | ||
A suspended fiber may be resumed in one of two ways: | A suspended fiber may be resumed in one of two ways: | ||
Line 148: | Line 155: | ||
* throwing an exception from '' | * throwing an exception from '' | ||
- | '' | + | '' |
- | '' | + | '' |
==== ReflectionFiber ==== | ==== ReflectionFiber ==== | ||
- | '' | + | '' |
<code php> | <code php> | ||
- | class ReflectionFiber | + | final class ReflectionFiber |
{ | { | ||
/** | /** | ||
Line 163: | Line 170: | ||
| | ||
*/ | */ | ||
- | public function __construct(Fiber $fiber) { } | + | public function __construct(Fiber $fiber) |
+ | |||
+ | /** | ||
+ | * @return Fiber The reflected Fiber object. | ||
+ | */ | ||
+ | public function getFiber(): Fiber {} | ||
/** | /** | ||
* @return string Current file of fiber execution. | * @return string Current file of fiber execution. | ||
- | * | ||
- | * @throws ReflectionException If the fiber has not been started or has terminated. | ||
*/ | */ | ||
- | public function getExecutingFile(): | + | public function getExecutingFile(): |
/** | /** | ||
* @return int Current line of fiber execution. | * @return int Current line of fiber execution. | ||
- | * | ||
- | * @throws ReflectionException If the fiber has not been started or has terminated. | ||
*/ | */ | ||
- | public function getExecutingLine(): | + | public function getExecutingLine(): |
/** | /** | ||
Line 184: | Line 192: | ||
* @return array Fiber backtrace, similar to {@see debug_backtrace()} | * @return array Fiber backtrace, similar to {@see debug_backtrace()} | ||
| | ||
- | * | ||
- | * @throws ReflectionException If the fiber has not been started or has terminated. | ||
*/ | */ | ||
- | public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): | + | public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): |
/** | /** | ||
* @return bool True if the fiber has been started. | * @return bool True if the fiber has been started. | ||
*/ | */ | ||
- | public function isStarted(): | + | public function isStarted(): |
/** | /** | ||
* @return bool True if the fiber is currently suspended. | * @return bool True if the fiber is currently suspended. | ||
*/ | */ | ||
- | public function isSuspended(): | + | public function isSuspended(): |
/** | /** | ||
* @return bool True if the fiber is currently running. | * @return bool True if the fiber is currently running. | ||
*/ | */ | ||
- | public function isRunning(): | + | public function isRunning(): |
/** | /** | ||
* @return bool True if the fiber has completed execution (either returning or | * @return bool True if the fiber has completed execution (either returning or | ||
- | | + | |
*/ | */ | ||
- | public function isTerminated(): | + | public function isTerminated(): |
} | } | ||
</ | </ | ||
Line 214: | Line 220: | ||
=== Unfinished Fibers === | === Unfinished Fibers === | ||
- | Fibers that are not finished (do not complete execution) are destroyed similarly to unfinished generators, executing any pending '' | + | Fibers that are not finished (do not complete execution) are destroyed similarly to unfinished generators, executing any pending '' |
=== Fiber Stacks === | === Fiber Stacks === | ||
- | Each fiber is allocated a separate C stack and VM stack on the heap. The C stack is allocated using '' | + | Each fiber is allocated a separate C stack and VM stack on the heap. The C stack is allocated using '' |
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
Declares '' | Declares '' | ||
- | |||
- | ===== Proposed PHP Version(s) ===== | ||
- | |||
- | PHP 8.1 | ||
===== Future Scope ===== | ===== Future Scope ===== | ||
- | === suspend keyword === | + | The current implementation does not provide an internal API for fibers for PHP extensions. This RFC focuses on the user space fiber API. An internal fiber API will be added, collaborating with other internal developers and using feedback from PHP extension developers, including Swoole, so fibers can be created and controlled from PHP extensions. An extension may still optionally provide their own custom fiber implementation, |
- | '' | + | ===== Proposed PHP Version(s) ===== |
- | // | + | PHP 8.1 |
- | + | ||
- | === async/await keywords === | + | |
- | + | ||
- | Using an internally defined event loop (or fiber scheduler) and an additionally defined '' | + | |
- | + | ||
- | An '' | + | |
- | + | ||
- | <code php> | + | |
- | $awaitable = async functionOrMethod(); | + | |
- | // async modifies the call to return an Awaitable, creating a new fiber, so execution continues immediately. | + | |
- | await $awaitable; // Await the function result at a later point. | + | |
- | </ | + | |
- | + | ||
- | These keywords can be created in user code using the proposed fiber API. [[https:// | + | |
- | + | ||
- | === defer keyword === | + | |
- | + | ||
- | Fibers may be used to implement a '' | + | |
- | + | ||
- | This keyword also can be created in user code using the proposed fiber API, an example being '' | + | |
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
Line 397: | Line 379: | ||
The next few examples use the async framework [[https:// | The next few examples use the async framework [[https:// | ||
- | amphp v3 uses an [[https:// | + | amphp v3 uses an [[https:// |
- | The '' | + | The '' |
<code php> | <code php> | ||
Line 429: | Line 411: | ||
---- | ---- | ||
- | The next example again uses amphp v3 to demonstrate how the event loop fiber continues executing while the main thread is " | + | The next example again uses amphp v3 to demonstrate how the event loop fiber continues executing while the main thread is " |
<code php> | <code php> | ||
Line 461: | Line 443: | ||
// await() suspends the fiber until the given promise (or array of promises here) are resolved. | // await() suspends the fiber until the given promise (or array of promises here) are resolved. | ||
$result = await([ | $result = await([ | ||
- | async('asyncTask', | + | async(fn() => asyncTask(2)), // async() creates a new fiber and returns a promise for the result. |
- | async('asyncTask', | + | async(fn() => asyncTask(3)), |
]); | ]); | ||
var_dump($result); | var_dump($result); | ||
Line 471: | Line 453: | ||
// array_map() takes 2 seconds to execute as the two calls are not concurrent, but this shows | // array_map() takes 2 seconds to execute as the two calls are not concurrent, but this shows | ||
// that fibers are supported by internal callbacks. | // that fibers are supported by internal callbacks. | ||
- | $result = array_map('asyncTask', [5, 6]); | + | $result = array_map(fn(int $value) => asyncTask($value), [5, 6]); |
var_dump($result); | var_dump($result); | ||
Line 479: | Line 461: | ||
---- | ---- | ||
- | Since fibers can be paused during calls within the PHP VM, fibers can also be used to create asynchronous iterators and generators. The example below uses amphp v3 to suspend a fiber within a generator, awaiting resolution of a '' | + | Since fibers can be paused during calls within the PHP VM, fibers can also be used to create asynchronous iterators and generators. The example below uses amphp v3 to suspend a fiber within a generator, awaiting resolution of a '' |
<code php> | <code php> | ||
Line 510: | Line 492: | ||
---- | ---- | ||
- | The example below shows how [[https:// | + | The example below shows how [[https:// |
<code php> | <code php> | ||
Line 519: | Line 501: | ||
{ | { | ||
$fiber = Fiber:: | $fiber = Fiber:: | ||
+ | if ($fiber === null) { | ||
+ | throw new Error(' | ||
+ | } | ||
$promise-> | $promise-> | ||
Line 525: | Line 510: | ||
); | ); | ||
- | return Fiber:: | + | return Fiber:: |
} | } | ||
</ | </ | ||
Line 561: | Line 546: | ||
Blocking code (such as '' | Blocking code (such as '' | ||
- | As fibers allow transparent use of asynchronous I/O, blocking implementations can be replaced by non-blocking implementations without affecting the entire call stack. If an internal | + | As fibers allow transparent use of asynchronous I/O, blocking implementations can be replaced by non-blocking implementations without affecting the entire call stack. If an internal |
=== How do various fibers access the same memory? === | === How do various fibers access the same memory? === | ||
Line 567: | Line 552: | ||
All fibers exist within a single thread. Only a single fiber may execute at a time, so memory cannot be accessed or modified simultaneously by multiple fibers, unlike threads which may modify memory simultaneously. | All fibers exist within a single thread. Only a single fiber may execute at a time, so memory cannot be accessed or modified simultaneously by multiple fibers, unlike threads which may modify memory simultaneously. | ||
- | As fibers are suspended and resumed, execution of multiple fibers that access the same memory can be interleaved. Thus a running fiber may modify memory depended upon by another suspended fiber. There are various strategies to address this problem, including mutexes, semaphores, memory parcels, and channels. This RFC does not provide any such implementations as these can be implemented in user space code using the proposed fiber API. Below is an example implementation of a mutex using the proposed fiber API and the example '' | + | As fibers are suspended and resumed, execution of multiple fibers that access the same memory can be interleaved. Thus a running fiber may modify memory depended upon by another suspended fiber. There are various strategies to address this problem, including mutexes, semaphores, memory parcels, and channels. This RFC does not provide any such implementations as these can be implemented in user space code using the proposed fiber API. |
- | + | ||
- | <code php> | + | |
- | class Mutex | + | |
- | { | + | |
- | private Loop $loop; | + | |
- | private SplQueue $queue; | + | |
- | private bool $locked = false; | + | |
- | + | ||
- | public function __construct(Loop $loop) | + | |
- | { | + | |
- | $this-> | + | |
- | $this-> | + | |
- | } | + | |
- | + | ||
- | public function acquire(): Lock | + | |
- | { | + | |
- | if ($this-> | + | |
- | $this-> | + | |
- | Fiber:: | + | |
- | } | + | |
- | + | ||
- | $this-> | + | |
- | + | ||
- | return new Lock(function (): void { | + | |
- | if ($this-> | + | |
- | $this-> | + | |
- | return; | + | |
- | } | + | |
- | + | ||
- | $fiber = $this-> | + | |
- | $this-> | + | |
- | }); | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | class Lock | + | |
- | { | + | |
- | private \Closure $release; | + | |
- | + | ||
- | public function __construct(\Closure $release) | + | |
- | { | + | |
- | $this-> | + | |
- | } | + | |
- | + | ||
- | public function __destruct() | + | |
- | { | + | |
- | $this-> | + | |
- | } | + | |
- | + | ||
- | public function release(): void | + | |
- | { | + | |
- | if (isset($this-> | + | |
- | $release = $this-> | + | |
- | unset($this-> | + | |
- | ($release)(); | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | </ | + | |
=== Why add this to PHP core? === | === Why add this to PHP core? === | ||
Line 640: | Line 566: | ||
It is the opinion of the authors of this RFC that it is best to provide the bare minimum in core and allow user code to implement other components as they desire. If the community moves toward a single event loop API or a need emerges for an event loop in PHP core, this can be done in a future RFC. Providing a core event loop without core functionality using it (such as streams, file access, etc.) would be misleading and confusing for users. Deferring such functionality to user frameworks and providing only a minimum API in core keeps expectations in check. | It is the opinion of the authors of this RFC that it is best to provide the bare minimum in core and allow user code to implement other components as they desire. If the community moves toward a single event loop API or a need emerges for an event loop in PHP core, this can be done in a future RFC. Providing a core event loop without core functionality using it (such as streams, file access, etc.) would be misleading and confusing for users. Deferring such functionality to user frameworks and providing only a minimum API in core keeps expectations in check. | ||
- | This RFC does not preclude adding async/await and an event loop to core, see [[# | + | This RFC does not preclude adding async/await and an event loop to core. |
=== How does this proposal differ from prior Fiber proposals? === | === How does this proposal differ from prior Fiber proposals? === | ||
Line 657: | Line 583: | ||
As noted in [[# | As noted in [[# | ||
+ | |||
+ | ===== Vote ===== | ||
+ | |||
+ | Voting started on 2021-03-08 and will run through 2021-03-22. 2/3 required to accept. | ||
+ | |||
+ | <doodle title=" | ||
+ | * Yes | ||
+ | * No | ||
+ | </ | ||
===== References ===== | ===== References ===== |
rfc/fibers.txt · Last modified: 2021/07/12 21:30 by kelunik