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/23 05:48] – Add question about fiber memory access trowski | rfc:fibers [2021/03/23 15:12] – Close vote kelunik | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== PHP RFC: Fibers ====== | ====== PHP RFC: Fibers ====== | ||
- | * Date: 2020-12-17 | + | * 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 '' | ||
- | ==== 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 multitasking in PHP. These are also known as coroutines or green-threads. | Fibers allow the creation of full-stack, interruptible functions that can be used to implement cooperative multitasking in PHP. These are also known as coroutines or green-threads. | ||
Line 41: | Line 39: | ||
Unlike stack-less Generators, each Fiber has its own call stack, allowing them to be paused within deeply nested function calls. A function declaring an interruption point (i.e., calling '' | Unlike stack-less Generators, each Fiber has its own call stack, allowing them to be paused within deeply nested function calls. A function declaring an interruption point (i.e., calling '' | ||
- | |||
- | This proposal treats '' | ||
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 64: | Line 56: | ||
* @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) {} |
/** | /** | ||
* 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. | ||
* | * | ||
- | * @throw FiberError If the fiber is running or terminated. | + | * @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. | * @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. | * @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. | * @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(): |
/** | /** | ||
- | | + | |
* | * | ||
- | | + | * @throws FiberError If the fiber has not terminated or the fiber threw an exception. |
- | | + | */ |
- | * @return | + | public function getReturn(): mixed {} |
- | * | + | |
- | * @throws FiberError Thrown | + | /** |
+ | * @return | ||
*/ | */ | ||
- | public static function this(): | + | public static function this(): |
/** | /** | ||
- | * Suspend execution of the fiber, switching execution to the scheduler. | + | * Suspend execution of the fiber. The fiber may be resumed with {@see Fiber:: |
* | * | ||
- | | + | |
- | * within the run() method of the instance of {@see FiberScheduler} given. | + | |
* | * | ||
- | | + | |
- | * | + | |
- | | + | |
* | * | ||
* @return mixed Value provided to {@see Fiber:: | * @return mixed Value provided to {@see Fiber:: | ||
* | * | ||
- | * @throws FiberError Thrown if within | + | * @throws FiberError Thrown if not within |
* @throws Throwable Exception provided to {@see Fiber:: | * @throws Throwable Exception provided to {@see Fiber:: | ||
*/ | */ | ||
- | public static function suspend(FiberScheduler | + | 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 165: | 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 207: | 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 228: | 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 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(): |
- | } | + | |
- | </ | + | |
- | + | ||
- | ==== ReflectionFiberScheduler ==== | + | |
- | + | ||
- | '' | + | |
- | + | ||
- | <code php> | + | |
- | class ReflectionFiberScheduler extends ReflectionFiber | + | |
- | { | + | |
- | /** | + | |
- | * @param FiberScheduler $scheduler | + | |
- | */ | + | |
- | public function __construct(FiberScheduler $scheduler) { } | + | |
- | + | ||
- | /** | + | |
- | * @return FiberScheduler The instance used to create the fiber. | + | |
- | */ | + | |
- | public function getScheduler(): | + | |
} | } | ||
</ | </ | ||
Line 277: | 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 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) ===== |
- | **suspend (** // | + | PHP 8.1 |
- | + | ||
- | Below is an example usage of the proposed API inside an object method, where the current '' | + | |
- | + | ||
- | <code php> | + | |
- | $this-> | + | |
- | return Fiber:: | + | |
- | </ | + | |
- | + | ||
- | The above could instead be written as the following using a '' | + | |
- | + | ||
- | <code php> | + | |
- | return 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 '' | + | |
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
Line 342: | 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::suspend(' |
- | private array $callbacks | + | echo "Value used to resume fiber: |
- | + | ||
- | /** | + | |
- | * Run the scheduler. | + | |
- | */ | + | |
- | public function run(): void | + | |
- | { | + | |
- | while (!empty($this-> | + | |
- | $callbacks = $this-> | + | |
- | $this-> | + | |
- | foreach ($callbacks as $id => $callback) { | + | |
- | $callback(); | + | |
- | } | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | /** | + | |
- | * Enqueue a callback to executed at a later time. | + | |
- | */ | + | |
- | public | + | |
- | | + | |
- | $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 later resumed in a fiber scheduler (event loop) upon an event. | + | |
- | + | ||
- | <code php> | + | |
- | $scheduler = new Scheduler; | + | |
- | + | ||
- | // Get a reference to the currently executing fiber ({main} in this case). | + | |
- | $fiber | + | |
- | + | ||
- | // This example 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 | + | |
- | $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-> |
- | $value = Fiber:: | + | |
- | echo "After resuming main fiber: ", $value, " | + | echo "Value from fiber suspending: ", $value, " |
- | </ | + | |
- | This example is expanded for clarity and comments, but may be condensed using short closures. | + | $fiber-> |
- | + | ||
- | <code php> | + | |
- | $scheduler = new Scheduler; | + | |
- | + | ||
- | $fiber = Fiber:: | + | |
- | $scheduler-> | + | |
- | $value = Fiber:: | + | |
- | + | ||
- | echo "After resuming main fiber: ", $value, " | + | |
</ | </ | ||
- | Fibers may also be resumed by throwing an exception. | + | This example will output the following: |
- | < | + | < |
- | $scheduler = new Scheduler; | + | Value from fiber suspending: |
- | + | Value used to resume fiber: | |
- | // Get a reference to the currently executing | + | |
- | $fiber = Fiber:: | + | |
- | + | ||
- | // This example instead throws an exception into the fiber to resume | + | |
- | + | ||
- | // 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; | ||
- | |||
- | $fiber = Fiber:: | ||
- | $scheduler-> | ||
- | |||
- | try { | ||
- | $value = Fiber:: | ||
- | } 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 520: | Line 335: | ||
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 = new Fiber(function () use ($scheduler, $read): void { | + | $fiber = new Fiber(function () use ($loop, $read): void { |
echo " | echo " | ||
$fiber = Fiber:: | $fiber = Fiber:: | ||
- | $scheduler-> | + | $loop-> |
- | Fiber:: | + | Fiber:: |
$data = fread($read, | $data = fread($read, | ||
Line 535: | Line 350: | ||
}); | }); | ||
- | // 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. |
- | $fiber = Fiber:: | + | $loop-> |
- | $scheduler-> | + | |
- | echo Fiber:: | + | |
- | // Write data in main thread once it is resumed. | + | // Run the event loop. |
- | fwrite($write, " | + | $loop-> |
</ | </ | ||
Line 551: | Line 364: | ||
< | < | ||
Waiting for data... | Waiting for data... | ||
- | Writing data... | ||
Received data: Hello, world! | Received data: Hello, world! | ||
</ | </ | ||
Line 557: | 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 | + | Below is a chart illustrating execution flow between '' |
{{ https:// | {{ https:// | ||
Line 563: | Line 375: | ||
---- | ---- | ||
- | The next example below uses '' | + | The next few examples use the async framework |
- | <code php> | + | amphp v3 uses an [[https:// |
- | $loop = new Loop; | + | |
- | $fiber = Fiber:: | + | |
- | $loop-> | + | |
- | var_dump(Fiber:: | + | |
- | </ | + | |
- | + | ||
- | 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 = new Fiber(function () use ($loop): void { | + | |
- | $fiber = Fiber:: | + | |
- | $loop-> | + | |
- | $value = Fiber:: | + | |
- | var_dump($value); | + | |
- | }); | + | |
- | $loop-> | + | |
- | + | ||
- | $fiber = new Fiber(function () use ($loop): void { | + | |
- | $fiber = Fiber:: | + | |
- | $loop-> | + | |
- | $value = Fiber:: | + | |
- | var_dump($value); | + | |
- | }); | + | |
- | $loop-> | + | |
- | + | ||
- | $fiber = new Fiber(function () use ($loop): void { | + | |
- | $fiber = Fiber:: | + | |
- | $loop-> | + | |
- | $value = Fiber:: | + | |
- | var_dump($value); | + | |
- | }); | + | |
- | $loop-> | + | |
- | + | ||
- | // Suspend the main thread to enter the FiberScheduler. | + | |
- | $fiber = Fiber:: | + | |
- | $loop-> | + | |
- | $value = Fiber:: | + | |
- | 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 '' | + | |
- | + | ||
- | <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 = new Fiber(function () use ($loop, $write): void { | + | |
- | $fiber = Fiber:: | + | |
- | + | ||
- | // Suspend fiber for 1 second. | + | |
- | echo " | + | |
- | $loop-> | + | |
- | Fiber:: | + | |
- | + | ||
- | // Write data to the socket once it is writable. | + | |
- | echo " | + | |
- | $loop-> | + | |
- | $bytes = Fiber:: | + | |
- | + | ||
- | echo "Wrote {$bytes} bytes.\n"; | + | |
- | }); | + | |
- | + | ||
- | $loop-> | + | |
- | + | ||
- | echo " | + | |
- | + | ||
- | // Read data in main fiber. | + | |
- | $fiber = Fiber:: | + | |
- | $loop-> | + | |
- | $data = Fiber:: | + | |
- | + | ||
- | echo " | + | |
- | </ | + | |
- | + | ||
- | The above code will output the following: | + | |
- | + | ||
- | < | + | |
- | Waiting for data... | + | |
- | Waiting for 1 second... | + | |
- | Writing data... | + | |
- | Wrote 13 bytes. | + | |
- | Received data: Hello, world! | + | |
- | </ | + | |
- | + | ||
- | For simplicity this example is reading and writing to 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 '' | + | |
- | This example is similar to the example above which created mutiple fibers that “slept” for differing times, 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 713: | Line 402: | ||
}); | }); | ||
- | // Suspend the main fiber with delay(). | + | // Suspend the main context |
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 753: | Line 441: | ||
// 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 763: | Line 451: | ||
// 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); | ||
$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 '' | + | 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 800: | Line 487: | ||
var_dump(...generator()); | var_dump(...generator()); | ||
</ | </ | ||
- | |||
---- | ---- | ||
- | The example below shows how [[https:// | + | The example below shows how [[https:// |
<code php> | <code php> | ||
Line 813: | Line 499: | ||
{ | { | ||
$fiber = Fiber:: | $fiber = Fiber:: | ||
+ | if ($fiber === null) { | ||
+ | throw new Error(' | ||
+ | } | ||
$promise-> | $promise-> | ||
Line 819: | Line 508: | ||
); | ); | ||
- | 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-> | ||
- | $this-> | ||
- | return Fiber:: | ||
- | } | ||
- | |||
- | if ($this-> | ||
- | throw $this-> | ||
- | } | ||
- | |||
- | return $this-> | ||
- | } | ||
- | |||
- | public function schedule(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-> | ||
- | } | ||
- | } | ||
- | } | ||
- | |||
- | 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-> | ||
- | |||
- | $this-> | ||
- | Fiber:: | ||
- | |||
- | $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 = new Fiber(function () use ($promise, $callable) { | ||
- | 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 1044: | Line 523: | ||
'' | '' | ||
- | |||
- | === 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, usually 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: | ||
- | |||
- | **The 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, will hurt interoperability, | ||
=== What about performance? | === What about performance? | ||
Line 1069: | 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 === | ||
Line 1077: | Line 544: | ||
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 1083: | Line 550: | ||
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 '' | + | 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 Scheduler $scheduler; | + | |
- | private SplQueue $queue; | + | |
- | private bool $locked = false; | + | |
- | + | ||
- | public function __construct(Scheduler $scheduler) | + | |
- | { | + | |
- | $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 1156: | Line 564: | ||
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? === | ||
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 1175: | Line 581: | ||
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