rfc:fibers
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionNext revisionBoth sides next revision | ||
rfc:fibers [2020/12/04 17:36] – Update from GitHub draft trowski | rfc:fibers [2021/03/23 15:12] – Close vote kelunik | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== PHP RFC: Fibers ====== | ====== PHP RFC: Fibers ====== | ||
- | * Date: 2020-12-04 | + | * Date: 2021-03-08 |
- | * Author: Aaron Piotrowski < | + | * Authors: Aaron Piotrowski <trowski@php.net>, |
- | * 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 '' | ||
- | ==== Definition | + | Fibers allow for transparent non-blocking I/O implementations |
- | To allow better understanding of the RFC, this section defines what the names Fiber and FiberScheduler mean for the RFC being proposed. | + | ==== Fibers ==== |
- | === Fibers === | + | Fibers allow the creation of full-stack, interruptible functions that can be used to implement cooperative |
- | + | ||
- | Fibers allow you to create | + | |
- | + | ||
- | Unlike stack-less Generators, each Fiber contains a call stack, allowing them to be paused within deeply nested function calls. A function declaring an interruption point (i.e., calling '' | + | |
Fibers pause the entire execution stack, so the direct caller of the function does not need to change how it invokes the function. | Fibers pause the entire execution stack, so the direct caller of the function does not need to change how it invokes the function. | ||
Line 42: | Line 38: | ||
Execution may be interrupted anywhere in the call stack using '' | Execution may be interrupted anywhere in the call stack using '' | ||
- | This proposal treats '' | + | Unlike stack-less Generators, each Fiber has its own call stack, allowing |
Fibers can be suspended in //any// function call, including those called from within the PHP VM, such as functions provided to '' | Fibers can be suspended in //any// function call, including those called from within the PHP VM, such as functions provided to '' | ||
Once suspended, execution of the fiber may be resumed with any value using '' | Once suspended, execution of the fiber may be resumed with any value using '' | ||
- | |||
- | === FiberScheduler === | ||
- | |||
- | A '' | ||
===== Proposal ===== | ===== Proposal ===== | ||
Line 62: | Line 54: | ||
{ | { | ||
/** | /** | ||
- | * @param callable $callback Function to invoke when running | + | * @param callable $callback Function to invoke when starting |
*/ | */ | ||
- | public | + | public function |
/** | /** | ||
* Starts execution of the fiber. Returns when the fiber suspends or terminates. | * Starts execution of the fiber. Returns when the fiber suspends or terminates. | ||
- | * | ||
- | * Must be called within {@see FiberScheduler:: | ||
* | * | ||
* @param mixed ...$args Arguments passed to fiber function. | * @param mixed ...$args Arguments passed to fiber function. | ||
+ | * | ||
+ | * @return mixed Value from the first suspension point or NULL if the fiber returns. | ||
+ | * | ||
+ | * @throw FiberError If the fiber has already been started. | ||
+ | * @throw Throwable If the fiber callable throws an uncaught exception. | ||
*/ | */ | ||
- | public function start(mixed ...$args): | + | public function start(mixed ...$args): |
/** | /** | ||
* Resumes the fiber, returning the given value from {@see Fiber:: | * Resumes the fiber, returning the given value from {@see Fiber:: | ||
* Returns when the fiber suspends or terminates. | * Returns when the fiber suspends or terminates. | ||
- | * | ||
- | * Must be called within {@see FiberScheduler:: | ||
* | * | ||
* @param mixed $value | * @param mixed $value | ||
* | * | ||
- | * @throw FiberError If the fiber is running or terminated. | + | * @return mixed Value from the next suspension point or NULL if the fiber returns. |
+ | * | ||
+ | * @throw FiberError If the fiber has not started, | ||
+ | * @throw Throwable If the fiber callable throws an uncaught exception. | ||
*/ | */ | ||
- | public function resume(mixed $value = null): | + | public function resume(mixed $value = null): |
/** | /** | ||
* Throws the given exception into the fiber from {@see Fiber:: | * Throws the given exception into the fiber from {@see Fiber:: | ||
* Returns when the fiber suspends or terminates. | * Returns when the fiber suspends or terminates. | ||
- | * | ||
- | * Must be called within {@see FiberScheduler:: | ||
* | * | ||
* @param Throwable $exception | * @param Throwable $exception | ||
* | * | ||
- | * @throw FiberError If the fiber is running or terminated. | + | * @return mixed Value from the next suspension point or NULL if the fiber returns. |
+ | * | ||
+ | * @throw FiberError If the fiber has not started, | ||
+ | * @throw Throwable If the fiber callable throws an uncaught exception. | ||
+ | */ | ||
+ | public function throw(Throwable $exception): | ||
+ | |||
+ | /** | ||
+ | * @return bool True if the fiber has been started. | ||
*/ | */ | ||
- | public function | + | public function |
/** | /** | ||
* @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(): |
/** | /** | ||
- | | + | |
- | * The fiber may be resumed with {@see Fiber:: | + | |
* | * | ||
- | | + | |
+ | */ | ||
+ | public function getReturn(): | ||
+ | |||
+ | /** | ||
+ | * @return self|null Returns the currently executing fiber instance or NULL if in {main}. | ||
+ | */ | ||
+ | public static function this(): ?self {} | ||
+ | |||
+ | /** | ||
+ | * Suspend execution of the fiber. The fiber may be resumed with {@see Fiber::resume()} or {@see Fiber:: | ||
+ | * | ||
+ | * Cannot be called from {main}. | ||
* | * | ||
- | * @param | + | * @param |
- | | + | |
* | * | ||
* @return mixed Value provided to {@see Fiber:: | * @return mixed Value provided to {@see Fiber:: | ||
* | * | ||
- | * @throws FiberError Thrown if within {@see FiberScheduler:: | + | * @throws FiberError Thrown if not within |
* @throws Throwable Exception provided to {@see Fiber:: | * @throws Throwable Exception provided to {@see Fiber:: | ||
*/ | */ | ||
- | public static function suspend(callable | + | public static function suspend(mixed $value = null): mixed {} |
- | + | ||
- | /** | + | |
- | * Private constructor to force use of {@see create()}. | + | |
- | */ | + | |
- | private function __construct() | + | |
} | } | ||
</ | </ | ||
- | 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 146: | Line 153: | ||
* throwing an exception from '' | * throwing an exception from '' | ||
- | ==== FiberScheduler ==== | + | '' |
- | <code php> | + | '' |
- | interface FiberScheduler | + | |
- | { | + | |
- | /** | + | |
- | * Run the scheduler, scheduling and responding to events. | + | |
- | * This method should not return until no futher pending events remain in the fiber scheduler. | + | |
- | */ | + | |
- | public function run(): void; | + | |
- | } | + | |
- | </ | + | |
- | + | ||
- | A '' | + | |
- | + | ||
- | When an instance of '' | + | |
- | + | ||
- | If a scheduler completes (that is, returns from '' | + | |
- | + | ||
- | If a '' | + | |
- | + | ||
- | '' | + | |
- | + | ||
- | A fiber //must// be resumed from within | + | |
- | + | ||
- | Fibers must be started and resumed within a fiber scheduler in order to maintain ordering of the internal fiber stack. Internally, a fiber may only switch to a new fiber, a suspended fiber, | + | |
- | + | ||
- | When a script ends, each scheduler fiber created from a call to '' | + | |
- | + | ||
- | This RFC does not include an implementation for '' | + | |
==== ReflectionFiber ==== | ==== ReflectionFiber ==== | ||
- | '' | + | '' |
<code php> | <code php> | ||
- | class ReflectionFiber | + | final class ReflectionFiber |
{ | { | ||
/** | /** | ||
Line 188: | Line 168: | ||
| | ||
*/ | */ | ||
- | 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 209: | Line 190: | ||
* @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 is currently suspended, false otherwise. | + | * @return bool True if the fiber has been started. |
*/ | */ | ||
- | public function | + | public function |
/** | /** | ||
- | * @return bool True if the fiber is currently | + | * @return bool True if the fiber is currently |
*/ | */ | ||
- | public function | + | public function |
/** | /** | ||
- | * @return bool True if the fiber has completed execution (either returning or | + | * @return bool True if the fiber is currently running. |
- | | + | |
*/ | */ | ||
- | public function | + | public function |
- | } | + | |
- | </ | + | |
- | ==== ReflectionFiberScheduler ==== | ||
- | |||
- | '' | ||
- | |||
- | <code php> | ||
- | class ReflectionFiberScheduler extends ReflectionFiber | ||
- | { | ||
/** | /** | ||
- | * @param FiberScheduler $scheduler | + | * @return bool True if the fiber has completed execution (either returning or |
- | * | + | |
- | * @throws ReflectionException If the {@see FiberScheduler} | + | |
- | */ | + | |
- | public function __construct(FiberScheduler $scheduler) { } | + | |
- | + | ||
- | /** | + | |
- | * @return FiberScheduler The instance used to create the fiber. | + | |
*/ | */ | ||
- | public function | + | public function |
} | } | ||
</ | </ | ||
Line 255: | Line 218: | ||
=== 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. 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) ===== |
- | **suspend** // | + | PHP 8.1 |
- | + | ||
- | Below would be an example usage inside an object method, assigning the '' | + | |
- | + | ||
- | <code php> | + | |
- | suspend $fiber to $scheduler { | + | |
- | $this-> | + | |
- | } | + | |
- | </ | + | |
- | + | ||
- | === async/await keywords === | + | |
- | + | ||
- | Using an internally 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 [[https:// | + | |
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
Line 313: | Line 244: | ||
Implementation and tests at [[https:// | Implementation and tests at [[https:// | ||
- | [[https:// | + | [[https:// |
- | [[https:// | + | [[https:// |
===== Examples ===== | ===== Examples ===== | ||
- | First let’s define a very simple '' | + | This first simple |
<code php> | <code php> | ||
- | class Scheduler implements FiberScheduler | + | $fiber = new Fiber(function (): void { |
- | { | + | $value = Fiber:: |
- | | + | |
- | | + | }); |
- | /** | + | $value = $fiber->start(); |
- | * Run the scheduler. | + | |
- | */ | + | |
- | public function run(): void | + | |
- | { | + | |
- | while (!empty($this-> | + | |
- | $callbacks | + | |
- | $this-> | + | |
- | foreach ($callbacks as $id => $callback) { | + | |
- | $callback(); | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | /** | + | echo "Value from fiber suspending: ", |
- | * Enqueue a callback to executed at a later time. | + | |
- | */ | + | |
- | public function defer(callable $callback): void | + | |
- | { | + | |
- | $this-> | + | |
- | } | + | |
- | } | + | |
- | </ | + | |
- | This scheduler does nothing really useful by itself, but will be used to demonstrate the basics of how a fiber may be suspended and scheduled to be resumed in a scheduler (event loop) upon an event. | + | $fiber-> |
- | + | ||
- | <code php> | + | |
- | $scheduler = new Scheduler; | + | |
- | + | ||
- | // This function will be executed within the current fiber before suspending. | + | |
- | $enqueue = function (Fiber $fiber) use ($scheduler): | + | |
- | // This simple defers a function that immediately resumes the fiber. | + | |
- | // Usually a fiber will be resumed in response to an event. | + | |
- | + | ||
- | // Create an event in the fiber scheduler to resume the fiber at a later time. | + | |
- | $scheduler-> | + | |
- | // Fibers must be resumed within FiberScheduler:: | + | |
- | // This closure will be executed within the loop in Scheduler:: | + | |
- | | + | |
- | }); | + | |
- | }; | + | |
- | + | ||
- | // Suspend the main fiber, which will be resumed later by the scheduler. | + | |
- | $value = Fiber:: | + | |
- | + | ||
- | echo "After resuming main fiber: ", $value, " | + | |
</ | </ | ||
- | This example | + | This example |
- | < | + | < |
- | $scheduler = new Scheduler; | + | Value from fiber suspending: fiber |
- | + | Value used to resume fiber: | |
- | $value = Fiber:: | + | |
- | + | ||
- | echo "After resuming main fiber: ", $value, " | + | |
</ | </ | ||
- | |||
- | Fibers may also be resumed by throwing an exception. | ||
- | |||
- | <code php> | ||
- | $scheduler = new Scheduler; | ||
- | |||
- | // This function will be executed within the current fiber before suspending. | ||
- | $enqueue = function (Fiber $fiber) use ($scheduler): | ||
- | // This example instead throws an exception into the fiber to resume it. | ||
- | |||
- | // Create an event in the fiber scheduler to throw into the fiber at a later time. | ||
- | $scheduler-> | ||
- | $fiber-> | ||
- | }); | ||
- | }; | ||
- | |||
- | try { | ||
- | // Suspend the main fiber, but this time it will be resumed with a thrown exception. | ||
- | $value = Fiber:: | ||
- | // The exception is thrown from the call to Fiber:: | ||
- | } catch (Exception $exception) { | ||
- | echo $exception-> | ||
- | } | ||
- | </ | ||
- | |||
- | Again this example may be condensed using short closures. | ||
- | |||
- | <code php> | ||
- | $scheduler = new Scheduler; | ||
- | |||
- | try { | ||
- | $value = Fiber:: | ||
- | fn() => $fiber-> | ||
- | ), $scheduler); | ||
- | } catch (Exception $exception) { | ||
- | echo $exception-> | ||
- | } | ||
- | </ | ||
- | |||
- | To be useful, rather than the scheduler immediately resuming the fiber, the scheduler should resume a fiber at a later time in response to an event. The next example demonstrates how a scheduler can resume a fiber in response to data becoming available on a socket. | ||
- | |||
---- | ---- | ||
- | The next example | + | The next example |
<code php> | <code php> | ||
- | class Scheduler implements FiberScheduler | + | class EventLoop |
{ | { | ||
private string $nextId = ' | private string $nextId = ' | ||
Line 479: | Line 325: | ||
} | } | ||
- | [$read, $write] = \stream_socket_pair( | + | [$read, $write] = stream_socket_pair( |
- | | + | stripos(PHP_OS, |
STREAM_SOCK_STREAM, | STREAM_SOCK_STREAM, | ||
STREAM_IPPROTO_IP | STREAM_IPPROTO_IP | ||
Line 486: | Line 332: | ||
// Set streams to non-blocking mode. | // Set streams to non-blocking mode. | ||
- | \stream_set_blocking($read, | + | stream_set_blocking($read, |
- | \stream_set_blocking($write, | + | stream_set_blocking($write, |
- | $scheduler | + | $loop = new EventLoop; |
// Read data in a separate fiber after checking if the stream is readable. | // Read data in a separate fiber after checking if the stream is readable. | ||
- | $fiber = Fiber::create(function () use ($scheduler, $read): void { | + | $fiber = new Fiber(function () use ($loop, $read): void { |
echo " | echo " | ||
- | | + | |
- | | + | $loop-> |
- | $scheduler | + | |
- | ); | + | |
- | $data = \fread($read, | + | $data = fread($read, |
echo " | echo " | ||
}); | }); | ||
- | // Start the new fiber within the fiber scheduler. | + | // Start the fiber, which will suspend while waiting for a read event. |
- | $scheduler-> | + | $fiber-> |
- | // Suspend main fiber to enter the scheduler. | + | // Defer writing data to an event loop callback. |
- | echo Fiber:: | + | $loop-> |
- | fn(Fiber $fiber) => $scheduler-> | + | |
- | $scheduler | + | |
- | ); | + | |
- | // Write data in main thread once it is resumed. | + | // Run the event loop. |
- | \fwrite($write, " | + | $loop-> |
</ | </ | ||
Line 522: | Line 364: | ||
< | < | ||
Waiting for data... | Waiting for data... | ||
- | Writing data... | ||
Received data: Hello, world! | Received data: Hello, world! | ||
</ | </ | ||
Line 528: | Line 369: | ||
If this example were written in a similar order without fibers, the script would be unable to read from a socket before writing to it, as the call to '' | If this example were written in a similar order without fibers, the script would be unable to read from a socket before writing to it, as the call to '' | ||
+ | Below is a chart illustrating execution flow between '' | ||
- | ---- | + | {{ https://wiki.php.net/_media/rfc/fiber-flow.png? |
- | + | ||
- | The next example below uses [[https://github.com/ | + | |
- | + | ||
- | <code php> | + | |
- | $loop = new Loop; | + | |
- | + | ||
- | $value = Fiber:: | + | |
- | $loop-> | + | |
- | }, $loop); | + | |
- | + | ||
- | var_dump($value); | + | |
- | </code> | + | |
- | + | ||
- | This example can be expanded to create multiple fibers, each with it’s own delay before resuming. | + | |
- | + | ||
- | <code php> | + | |
- | $loop = new Loop; | + | |
- | + | ||
- | // Create three new fibers and run them in the FiberScheduler. | + | |
- | $fiber = Fiber:: | + | |
- | $value = Fiber:: | + | |
- | $loop-> | + | |
- | }, $loop); | + | |
- | var_dump($value); | + | |
- | }); | + | |
- | $loop-> | + | |
- | + | ||
- | $fiber = Fiber:: | + | |
- | $value = Fiber:: | + | |
- | $loop-> | + | |
- | | + | |
- | var_dump($value); | + | |
- | }); | + | |
- | $loop-> | + | |
- | + | ||
- | $fiber = Fiber:: | + | |
- | $value = Fiber:: | + | |
- | $loop-> | + | |
- | }, $loop); | + | |
- | var_dump($value); | + | |
- | }); | + | |
- | $loop-> | + | |
- | + | ||
- | // Suspend the main thread to enter the FiberScheduler. | + | |
- | $value = Fiber:: | + | |
- | $loop-> | + | |
- | }, $loop); | + | |
- | var_dump($value); | + | |
- | </ | + | |
- | + | ||
- | The above code will output the following: | + | |
- | + | ||
- | < | + | |
- | int(4) | + | |
- | int(2) | + | |
- | int(1) | + | |
- | int(3) | + | |
- | </ | + | |
- | + | ||
- | Total execution time for the script is 2 seconds (2000ms) as this is the longest delay (sleep) defined. A similar synchronous script would take 5 seconds to execute as each delay would be in series rather than concurrent. | + | |
- | + | ||
- | While a contrived example, imagine if each of the fibers was awaiting data on a network socket or the result of a database query. Combining this with the ability to simultaneously run and suspend many fibers allows a single PHP process to concurrently await many events. | + | |
---- | ---- | ||
- | This example again uses [[https:// | + | The next few examples use the async framework [[https:// |
- | + | ||
- | <code php> | + | |
- | [$read, $write] = \stream_socket_pair( | + | |
- | \stripos(PHP_OS, | + | |
- | STREAM_SOCK_STREAM, | + | |
- | STREAM_IPPROTO_IP | + | |
- | ); | + | |
- | + | ||
- | // Set streams to non-blocking mode. | + | |
- | \stream_set_blocking($read, | + | |
- | \stream_set_blocking($write, | + | |
- | + | ||
- | $loop = new Loop; | + | |
- | + | ||
- | // Write data in a separate fiber after a 1 second delay. | + | |
- | $fiber = Fiber:: | + | |
- | // Suspend fiber for 1 second. | + | |
- | echo " | + | |
- | Fiber:: | + | |
- | $loop-> | + | |
- | }, $loop); | + | |
- | + | ||
- | // Write data to the socket once it is writable. | + | |
- | echo " | + | |
- | Fiber:: | + | |
- | $loop-> | + | |
- | }, $loop); | + | |
- | + | ||
- | echo "Write fiber finished.\n"; | + | |
- | }); | + | |
- | + | ||
- | $loop-> | + | |
- | + | ||
- | echo " | + | |
- | + | ||
- | // Read data in main fiber. | + | |
- | $data = Fiber:: | + | |
- | $loop-> | + | |
- | }, $loop); | + | |
- | + | ||
- | echo " | + | |
- | </ | + | |
- | + | ||
- | The above code will output the following: | + | |
- | + | ||
- | < | + | |
- | Waiting for data... | + | |
- | Waiting for 1 second... | + | |
- | Writing data... | + | |
- | Write fiber finished. | + | |
- | Received data: Hello, world! | + | |
- | </ | + | |
- | + | ||
- | For simplicity this example is reading and writing to the an internally connected socket, but similar code could read and write from many network sockets simultaneously. | + | |
- | + | ||
- | + | ||
- | ---- | + | |
- | + | ||
- | The next few examples use the async framework [[https:// | + | |
- | + | ||
- | AMPHP v3 uses an [[https:// | + | |
- | The [[https:// | + | amphp v3 uses an [[https:// |
- | This example is similar to the example above which creating mutiple fibers with different delays, but the underlying Fiber API is abstracted away into an API specific to the Amp framework. Note again this code is specific to AMPHP v3 and not part of this RFC, other frameworks may choose to implement this behavior in a different way. | + | The '' |
<code php> | <code php> | ||
Line 667: | Line 385: | ||
use function Amp\delay; | use function Amp\delay; | ||
+ | // defer() creates a new fiber and starts it when the | ||
+ | // current fiber is suspended or terminated. | ||
defer(function (): void { | defer(function (): void { | ||
delay(1500); | delay(1500); | ||
Line 682: | Line 402: | ||
}); | }); | ||
+ | // Suspend the main context with delay(). | ||
delay(500); | delay(500); | ||
var_dump(4); | var_dump(4); | ||
</ | </ | ||
- | |||
---- | ---- | ||
- | The next example again uses AMPHP v3 to demonstrate how the '' | + | The next example again uses amphp v3 to demonstrate how the event loop fiber continues executing while the main thread is "suspended". The '' |
<code php> | <code php> | ||
Line 697: | Line 417: | ||
use function Amp\delay; | use function Amp\delay; | ||
- | // Note that the function declares int as a return type, not Promise or Generator, but executes as a coroutine. | + | // Note that the function declares int as a return type, not Promise or Generator, |
+ | // but executes as a coroutine. | ||
function asyncTask(int $id): int { | function asyncTask(int $id): int { | ||
// Nothing useful is done here, but rather acts as a substitute for async I/O. | // Nothing useful is done here, but rather acts as a substitute for async I/O. | ||
- | delay(1000); | + | delay(1000); |
return $id; | return $id; | ||
} | } | ||
Line 713: | Line 434: | ||
}); | }); | ||
- | // Invoking | + | // Invoking |
- | $result = asyncTask(1); | + | $result = asyncTask(1); |
var_dump($result); | var_dump($result); | ||
- | // Simultaneously runs two new green threads, await their resolution in this green thread. | + | // Simultaneously runs two new fibers, await their resolution in the main fiber. |
+ | // 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 727: | Line 449: | ||
var_dump($result); | var_dump($result); | ||
- | // array_map() takes 2 seconds to execute as the calls are not concurrent, but this shows that fibers are | + | // array_map() takes 2 seconds to execute as the two calls are not concurrent, but this shows |
- | // 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); | ||
$running = false; // Stop the loop in the fiber created with defer() above. | $running = false; // Stop the loop in the fiber created with defer() above. | ||
</ | </ | ||
- | |||
---- | ---- | ||
- | 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 [[https:// | + | 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 757: | Line 478: | ||
} | } | ||
- | $generator | + | // Iterate over the generator |
- | + | // be suspended and resumed as needed. | |
- | foreach ($generator as $value) { | + | foreach (generator() as $value) { |
printf(" | printf(" | ||
} | } | ||
- | </ | ||
+ | // Argument unpacking also can use a suspending generator. | ||
+ | var_dump(...generator()); | ||
+ | </ | ||
---- | ---- | ||
- | The example below shows how [[https:// | + | The example below shows how [[https:// |
<code php> | <code php> | ||
Line 775: | Line 498: | ||
function await(PromiseInterface $promise, LoopInterface $loop): mixed | function await(PromiseInterface $promise, LoopInterface $loop): mixed | ||
{ | { | ||
- | $enqueue | + | $fiber = Fiber::this(); |
+ | if ($fiber === null) { | ||
+ | throw new Error(' | ||
+ | } | ||
+ | |||
+ | | ||
fn(mixed $value) => $loop-> | fn(mixed $value) => $loop-> | ||
- | fn(Throwable $reason) => $loop-> | + | fn(Throwable $reason) => $loop-> |
- | | + | ); |
- | return Fiber:: | + | return Fiber:: |
} | } | ||
</ | </ | ||
A demonstration of integrating ReactPHP with fibers has been implemented in [[https:// | A demonstration of integrating ReactPHP with fibers has been implemented in [[https:// | ||
- | |||
- | |||
- | ---- | ||
- | |||
- | The final example uses the '' | ||
- | |||
- | <code php> | ||
- | class Promise | ||
- | { | ||
- | /** @var Fiber[] */ | ||
- | private array $fibers = []; | ||
- | private Scheduler $scheduler; | ||
- | private bool $resolved = false; | ||
- | private ?\Throwable $error = null; | ||
- | private mixed $result; | ||
- | |||
- | public function __construct(Scheduler $scheduler) | ||
- | { | ||
- | $this-> | ||
- | } | ||
- | |||
- | public function await(): mixed | ||
- | { | ||
- | if (!$this-> | ||
- | return \Fiber:: | ||
- | } | ||
- | |||
- | if ($this-> | ||
- | throw $this-> | ||
- | } | ||
- | |||
- | return $this-> | ||
- | } | ||
- | |||
- | public function __invoke(Fiber $fiber): void | ||
- | { | ||
- | if ($this-> | ||
- | if ($this-> | ||
- | $this-> | ||
- | } else { | ||
- | $this-> | ||
- | } | ||
- | |||
- | return; | ||
- | } | ||
- | |||
- | $this-> | ||
- | } | ||
- | |||
- | public function resolve(mixed $value = null): void | ||
- | { | ||
- | if ($this-> | ||
- | throw new Error(" | ||
- | } | ||
- | |||
- | $this-> | ||
- | $this-> | ||
- | } | ||
- | |||
- | public function fail(Throwable $error): void | ||
- | { | ||
- | if ($this-> | ||
- | throw new Error(" | ||
- | } | ||
- | |||
- | $this-> | ||
- | $this-> | ||
- | } | ||
- | |||
- | private function continue(): void | ||
- | { | ||
- | $this-> | ||
- | |||
- | $fibers = $this-> | ||
- | $this-> | ||
- | |||
- | foreach ($fibers as $fiber) { | ||
- | ($this)($fiber); | ||
- | } | ||
- | } | ||
- | } | ||
- | |||
- | class Scheduler implements FiberScheduler | ||
- | { | ||
- | /** @var resource */ | ||
- | private $curl; | ||
- | /** @var callable[] */ | ||
- | private array $defers = []; | ||
- | /** @var Fiber[] */ | ||
- | private array $fibers = []; | ||
- | |||
- | public function __construct() | ||
- | { | ||
- | $this-> | ||
- | } | ||
- | |||
- | public function __destruct() | ||
- | { | ||
- | curl_multi_close($this-> | ||
- | } | ||
- | |||
- | public function fetch(string $url): string | ||
- | { | ||
- | $curl = curl_init(); | ||
- | |||
- | curl_setopt($curl, | ||
- | curl_setopt($curl, | ||
- | curl_setopt($curl, | ||
- | |||
- | curl_multi_add_handle($this-> | ||
- | |||
- | Fiber:: | ||
- | $this-> | ||
- | }, $this); | ||
- | |||
- | $status = curl_getinfo($curl, | ||
- | if ($status !== 200) { | ||
- | throw new Exception(\sprintf(' | ||
- | } | ||
- | |||
- | $body = substr(trim(curl_multi_getcontent($curl)), | ||
- | |||
- | curl_close($curl); | ||
- | |||
- | return $body; | ||
- | } | ||
- | |||
- | public function defer(callable $callable): void | ||
- | { | ||
- | $this-> | ||
- | } | ||
- | |||
- | public function async(callable $callable): Promise | ||
- | { | ||
- | $promise = new Promise($this); | ||
- | |||
- | $fiber = Fiber:: | ||
- | try { | ||
- | $promise-> | ||
- | } catch (\Throwable $e) { | ||
- | $promise-> | ||
- | } | ||
- | }); | ||
- | |||
- | $this-> | ||
- | |||
- | return $promise; | ||
- | } | ||
- | |||
- | public function run(): void | ||
- | { | ||
- | do { | ||
- | do { | ||
- | $defers = $this-> | ||
- | $this-> | ||
- | |||
- | foreach ($defers as $callable) { | ||
- | $callable(); | ||
- | } | ||
- | |||
- | $status = curl_multi_exec($this-> | ||
- | if ($active) { | ||
- | $select = curl_multi_select($this-> | ||
- | if ($select > 0) { | ||
- | $this-> | ||
- | } | ||
- | } | ||
- | } while ($active && $status === CURLM_OK); | ||
- | |||
- | $this-> | ||
- | } while ($this-> | ||
- | } | ||
- | |||
- | private function processQueue(): | ||
- | { | ||
- | while ($info = curl_multi_info_read($this-> | ||
- | if ($info[' | ||
- | continue; | ||
- | } | ||
- | |||
- | $fiber = $this-> | ||
- | $fiber-> | ||
- | } | ||
- | } | ||
- | } | ||
- | |||
- | function await(Promise ...$promises): | ||
- | { | ||
- | return array_map(fn($promise) => $promise-> | ||
- | } | ||
- | |||
- | $urls = array_fill(0, | ||
- | |||
- | $scheduler = new Scheduler; | ||
- | |||
- | $promises = []; | ||
- | foreach ($urls as $url) { | ||
- | $promises[] = $scheduler-> | ||
- | } | ||
- | |||
- | print ' | ||
- | |||
- | $start = hrtime(true); | ||
- | |||
- | $responses = await(...$promises); | ||
- | |||
- | // var_dump($responses); | ||
- | |||
- | print ((hrtime(true) - $start) / 1_000_000) . ' | ||
- | </ | ||
===== FAQ ===== | ===== FAQ ===== | ||
Line 1002: | Line 520: | ||
Fibers are an advanced feature that most users will not use directly. This feature is primarily targeted at library and framework authors to provide an event loop and an asynchronous programming API. Fibers allow integrating asynchronous code execution seamlessly into synchronous code at any point without the need to modify the application call stack or add boilerplate code. | Fibers are an advanced feature that most users will not use directly. This feature is primarily targeted at library and framework authors to provide an event loop and an asynchronous programming API. Fibers allow integrating asynchronous code execution seamlessly into synchronous code at any point without the need to modify the application call stack or add boilerplate code. | ||
- | **Fibers are not expected to be used in application-level code. Fibers provide a basic, low-level flow-control API to create higher-level abstractions that are then used in application code.** | + | **The Fiber API is not expected to be used directly |
'' | '' | ||
- | |||
- | === Why use FiberScheduler instead of an API similar to Lua or Ruby? === | ||
- | |||
- | Fibers require a scheduler to be useful. A scheduler is responsible for creating and resuming fibers. A fiber on it’s own does nothing – something external to the fiber must control it. This is not unlike generators. When you iterate over a generator using '' | ||
- | |||
- | **Fibers may suspend deep within the call stack, perhaps within library code authored by another. Therefore it makes sense to move control of the scheduler used to the point of fiber suspension, rather than at fiber creation.** | ||
- | |||
- | Additionally, | ||
- | |||
- | * Suspension of the top-level ('' | ||
- | * Nesting schedulers: A fiber may suspend into different fiber schedulers at various suspension points. Each scheduler will be started/ | ||
- | * Elimination of boilerplate: | ||
- | |||
- | **Any Fiber API is a low-level method of flow-control that is aimed at library and framework authors. A “simpler” API will not lead to average developers using fibers and will hurt interoperability and require more work for the average developer.** | ||
=== What about performance? | === What about performance? | ||
- | Switching between fibers is lightweight, | + | Switching between fibers is lightweight, |
=== What platforms are supported? === | === What platforms are supported? === | ||
Line 1030: | Line 534: | ||
'' | '' | ||
- | === How is a FiberScheduler implemented? === | + | === How are execution stacks swapped? === |
- | A '' | + | Each fiber holds a pointer to a C stack and a VM stack ('' |
+ | |||
+ | Functions such as '' | ||
=== How does blocking code affect fibers === | === How does blocking code affect fibers === | ||
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 event loop is available in the future, internal functions such as '' | ||
+ | |||
+ | === How do various fibers access the same memory? === | ||
+ | |||
+ | 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. | ||
=== Why add this to PHP core? === | === Why add this to PHP core? === | ||
Line 1043: | Line 557: | ||
Extensions that profile code need to account for switching fibers when creating backtraces and calculating execution times. This needs to be provided as a core internal API so any profiler could support fibers. The internal API that would be provided is out of scope of this RFC as it would not affect user code. | Extensions that profile code need to account for switching fibers when creating backtraces and calculating execution times. This needs to be provided as a core internal API so any profiler could support fibers. The internal API that would be provided is out of scope of this RFC as it would not affect user code. | ||
- | |||
- | Futher, the extension currently uses the observer API to determine when fiber schedulers are run to completion, however the timing is not ideal, as it occurs //before// shutdown functions and destructors are executed. Adding the fibers to PHP core would allow the engine to finish executing fiber schedulers //after// registered shutdown functions are invoked. | ||
=== Why not add an event loop and async/await API to core? === | === Why not add an event loop and async/await API to core? === | ||
Line 1050: | Line 562: | ||
This RFC proposes only the bare minimum required to allow user code to implement full-stack coroutines or green-threads in PHP. There are several frameworks that implement their own event loop API, promises, and other asynchronous APIs. These APIs vary greatly and are opinionated, | This RFC proposes only the bare minimum required to allow user code to implement full-stack coroutines or green-threads in PHP. There are several frameworks that implement their own event loop API, promises, and other asynchronous APIs. These APIs vary greatly and are opinionated, | ||
- | It is the opinion of the author | + | It is the opinion of the authors |
- | 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? === | ||
The prior [[https:// | The prior [[https:// | ||
- | |||
- | The API proposed here also differs, allowing suspension of the main context. | ||
=== Are fibers compatible with extensions, including Xdebug? === | === Are fibers compatible with extensions, including Xdebug? === | ||
Line 1070: | Line 580: | ||
* [[https:// | * [[https:// | ||
- | As noted in [[#why-add-this-to-php-core|“Why add this to PHP core?”]], extensions that profile code, create backtraces, provide execution times, etc. will need to be updated to account for switching between fibers to provide correct data. | + | As noted in [[#why_add_this_to_php_core|“Why add this to PHP core?”]], extensions that profile code, create backtraces, provide execution times, etc. will need to be updated to account for switching between fibers to provide correct data. |
+ | |||
+ | ===== 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