rfc:fibers

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
rfc:fibers [2021/02/02 20:44]
kelunik
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 <trowski@php.net>, Niklas Keller <kelunik@php.net>   * Authors: Aaron Piotrowski <trowski@php.net>, Niklas Keller <kelunik@php.net>
-  * Status: Under Discussion+  * Status: Implemented
   * First Published at: http://wiki.php.net/rfc/fibers   * First Published at: http://wiki.php.net/rfc/fibers
  
 ===== 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 that run synchronously. Synchronous functions stop execution until a result is available to return from the function.+For most of PHP’s history, people have written PHP code only as synchronous code. Execution of functions stops until a result is available to return from the function, including for I/O operations, which can be quite slow.
  
-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://amphp.org/|amphp]], [[https://reactphp.org/|ReactPHP]], and [[https://guzzlephp.org/|Guzzle]].+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://amphp.org/|amphp]], [[https://reactphp.org/|ReactPHP]], and [[https://guzzlephp.org/|Guzzle]].
  
 The problem this RFC seeks to address is a difficult one to explain, but can be referred to as the [[https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/|“What color is your function?”]] problem. The problem this RFC seeks to address is a difficult one to explain, but can be referred to as the [[https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/|“What color is your function?”]] problem.
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 callstack to be asynchronous+  * Calling an asynchronous function requires the entire call stack to be asynchronous
  
-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://en.wikipedia.org/wiki/Fiber_(computer_science)|Fibers]] to PHP.   * Adding support for [[https://en.wikipedia.org/wiki/Fiber_(computer_science)|Fibers]] to PHP.
-  * Adding ''Fiber'', ''FiberScheduler'', and the corresponding reflection classes ''ReflectionFiber'' and ''ReflectionFiberScheduler''.+  * Adding ''Fiber'' class and the corresponding reflection class ''ReflectionFiber''.
   * Adding exception classes ''FiberError'' and ''FiberExit'' to represent errors.   * Adding exception classes ''FiberError'' and ''FiberExit'' to represent errors.
  
-==== Definition of terms ====+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.
  
-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 ''Fiber::suspend()'') need not change its return type, unlike a function using ''yield'' which must return a ''Generator'' instance. 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 ''Fiber::suspend()'') need not change its return type, unlike a function using ''yield'' which must return a ''Generator'' instance.
- 
-This proposal treats ''{main}'' as a fiber, allowing ''Fiber::suspend()'' to be called from the top-level context. 
  
 Fibers can be suspended in //any// function call, including those called from within the PHP VM, such as functions provided to ''array_map'' or methods called by ''foreach'' on an ''Iterator'' object. Fibers can be suspended in //any// function call, including those called from within the PHP VM, such as functions provided to ''array_map'' or methods called by ''foreach'' on an ''Iterator'' object.
  
 Once suspended, execution of the fiber may be resumed with any value using ''Fiber->resume()'' or by throwing an exception into the fiber using ''Fiber->throw()''. The value is returned (or exception thrown) from ''Fiber::suspend()''. Once suspended, execution of the fiber may be resumed with any value using ''Fiber->resume()'' or by throwing an exception into the fiber using ''Fiber->throw()''. The value is returned (or exception thrown) from ''Fiber::suspend()''.
- 
-=== FiberScheduler === 
- 
-A ''FiberScheduler'' is able to start new fibers and resume suspended fibers. A fiber scheduler will generally act as an event loop, responding to events on sockets, timers, and deferred functions. When a fiber is suspended, execution switches into the fiber scheduler to await events or resume other suspended fibers. 
  
 ===== Proposal ===== ===== Proposal =====
Line 57: 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:
 +
 +<blockquote>Fiber::this() has been renamed to Fiber::getCurrent() during the PHP 8.1 alpha release phase.</blockquote>
  
 <code php> <code php>
Line 64: 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) {}
  
     /**     /**
      * 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 from {@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): void { }+    public function start(mixed ...$args): mixed {}
  
     /**     /**
      * Resumes the fiber, returning the given value from {@see Fiber::suspend()}.      * Resumes the fiber, returning the given value from {@see Fiber::suspend()}.
      * Returns when the fiber suspends or terminates.      * Returns when the fiber suspends or terminates.
-     * 
-     * Must be called from {@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, is runningor has terminated.
      * @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): void { }+    public function resume(mixed $value = null): mixed {}
  
     /**     /**
      * Throws the given exception into the fiber from {@see Fiber::suspend()}.      * Throws the given exception into the fiber from {@see Fiber::suspend()}.
      * Returns when the fiber suspends or terminates.      * Returns when the fiber suspends or terminates.
-     * 
-     * Must be called from {@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, is runningor has terminated.
      * @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): void { }+    public function throw(Throwable $exception): mixed {}
  
     /**     /**
      * @return bool True if the fiber has been started.      * @return bool True if the fiber has been started.
      */      */
-    public function isStarted(): bool { }+    public function isStarted(): bool {}
  
     /**     /**
      * @return bool True if the fiber is suspended.      * @return bool True if the fiber is suspended.
      */      */
-    public function isSuspended(): bool { }+    public function isSuspended(): bool {}
  
     /**     /**
      * @return bool True if the fiber is currently running.      * @return bool True if the fiber is currently running.
      */      */
-    public function isRunning(): bool { }+    public function isRunning(): bool {}
  
     /**     /**
-     * @return bool True if the fiber has completed execution.+     * @return bool True if the fiber has completed execution (returned or threw).
      */      */
-    public function isTerminated(): bool { }+    public function isTerminated(): bool {}
  
     /**     /**
-     Returns the currently executing Fiber instance.+     @return mixed Return value of the fiber callback. NULL is returned if the fiber does not have a return statement.
      *      *
-     Cannot be called from {@see FiberScheduler}+     * @throws FiberError If the fiber has not terminated or the fiber threw an exception
-     * +     */ 
-     * @return Fiber The currently executing fiber+    public function getReturn(): mixed {} 
-     * + 
-     * @throws FiberError Thrown if within {@see FiberScheduler}.+    /*
 +     * @return self|null Returns the currently executing fiber instance or NULL if in {main}.
      */      */
-    public static function this(): Fiber { }+    public static function this(): ?self {}
  
     /**     /**
-     * Suspend execution of the fiber, switching execution to the scheduler.+     * Suspend execution of the fiber. The fiber may be resumed with {@see Fiber::resume()} or {@see Fiber::throw()}.
      *      *
-     The fiber may be resumed with {@see Fiber::resume()} or {@see Fiber::throw()} +     Cannot be called from {main}.
-     from the instance of {@see FiberSchedulergiven.+
      *      *
-     Cannot be called from {@see FiberScheduler}+     @param mixed $value Value to return from {@see Fiber::resume()or {@see Fiber::throw()}.
-     * +
-     @param FiberScheduler $scheduler+
      *      *
      * @return mixed Value provided to {@see Fiber::resume()}.      * @return mixed Value provided to {@see Fiber::resume()}.
      *      *
-     * @throws FiberError Thrown if within {@see FiberScheduler}.+     * @throws FiberError Thrown if not within a fiber (i.e., if called from {main}).
      * @throws Throwable Exception provided to {@see Fiber::throw()}.      * @throws Throwable Exception provided to {@see Fiber::throw()}.
      */      */
-    public static function suspend(FiberScheduler $scheduler): mixed { }+    public static function suspend(mixed $value = null): mixed {}
 } }
 </code> </code>
  
-A ''Fiber'' object is created using ''new Fiber(callable $callback)'' with any callable. The callable need not call ''Fiber::suspend()'' directly, it may be in a deeply nested call, far down the call stack (or perhaps never call ''Fiber::suspend()'' at all). The returned ''Fiber'' may be started within a ''FiberScheduler'' (discussed below) using ''Fiber->start(mixed ...$args)'' with a variadic argument list that is provided as arguments to the callable used when creating the ''Fiber''.+A ''Fiber'' object is created using ''new Fiber(callable $callback)'' with any callable. The callable need not call ''Fiber::suspend()'' directly, it may be in a deeply nested call, far down the call stack (or perhaps never call ''Fiber::suspend()'' at all). The new ''Fiber'' may be started using ''Fiber->start(mixed ...$args)'' with a variadic argument list that is provided as arguments to the callable used when creating the ''Fiber''.
  
-''Fiber::suspend()'' suspends execution of the current fiber and switches to a scheduler fiber created from the instance of ''FiberScheduler'' given. This will be discussed in further detail in the next section describing ''FiberScheduler''+''Fiber::suspend()'' suspends execution of the current fiber and returns execution to the call to ''Fiber->start()''''Fiber->resume()'', or ''Fiber->throw()''Consider ''Fiber::suspend()'' to be similar to a generator using ''yield'', which returns execution to the call that advanced the generator.
- +
-''Fiber::this()'' returns the currently executing ''Fiber'' instanceThis object may be stored to be used at a later time to resume the fiber with any value or throw an exception into the fiber. The ''Fiber'' object should be attached to event watchers in the ''FiberScheduler'' instance (event loop)added to a list of pending fibers, or otherwise used to set up logic that will resume the fiber at a later time from the instance of ''FiberScheduler'' provided to ''Fiber::suspend()''.+
  
 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 155:
   * throwing an exception from ''Fiber::suspend()'' using ''Fiber->throw()''   * throwing an exception from ''Fiber::suspend()'' using ''Fiber->throw()''
  
-==== FiberScheduler ====+''Fiber->getReturn()'' returns the value returned from a terminated fiber (''NULL'' is returned if the fiber did not return a value). This function will throw an instance of ''FiberError'' if the fiber has not completed execution or threw an exception.
  
-<code php> +''Fiber::this()'' returns the currently executing ''Fiber'' instance or ''NULL'' if called from ''{main}''This allows a fiber to store reference to itself elsewheresuch as within an event loop callback or an array of awaiting fibers.
-final class FiberScheduler +
-+
-    /** +
-     * @param callable $callback Function to invoke when starting the fiber scheduler. +
-     */ +
-    public function __construct(callable $callback) { } +
- +
-    /** +
-     * @return bool True if the fiber has been started. +
-     */ +
-    public function isStarted(): bool { } +
- +
-    /** +
-     * @return bool True if the fiber is suspended. +
-     */ +
-    public function isSuspended(): bool { } +
- +
-    /** +
-     * @return bool True if the fiber is currently running. +
-     */ +
-    public function isRunning(): bool { } +
- +
-    /** +
-     * @return bool True if the fiber has completed execution. +
-     */ +
-    public function isTerminated(): bool { } +
-+
-</code> +
- +
-A ''FiberScheduler'' is able to start new fibers using ''Fiber->start()'' and resume fibers using ''Fiber->resume()'' and ''Fiber->throw()''. In general, a fiber scheduler would be created using a callback that runs an event loop that responds to events on sockets, timers, and deferred functions. +
- +
-An instance of ''FiberScheduler'' is a special kind of fiber. The scheduler fiber is suspended when resuming or starting another fiber (that is, when calling ''Fiber->start()''''Fiber->resume()'', or ''Fiber->throw()'') and again resumed when the same instance of ''FiberScheduler'' is provided to another call to ''Fiber::suspend()''It is expected that the ''FiberScheduler'' fiber will not terminate until all pending events have been processed and any suspended fibers have been resumed. In practice this is not difficult, as the scheduler fiber is suspended when resuming a fiber and only re-entered upon a fiber suspending which will create more events in the scheduler. +
- +
-If a scheduler terminates without resuming a dependent suspended fiber, an instance of ''FiberError'' is thrown from the call to ''Fiber::suspend()''. If ''FiberScheduler'' instance is later reused in a call to ''Fiber::suspend()''it must be ensured that the ''FiberScheduler'' did not already terminate. +
- +
-If the callback provided to ''FiberScheduler'' throws an exception, this results in an uncaught ''FiberExit'' exception and exits the script. +
- +
-A fiber //must// be resumed from within the instance of ''FiberScheduler'' provided to ''Fiber::suspend()''. Doing otherwise results in a fatal error. In practice this means that calling ''Fiber->resume()'' or ''Fiber->throw()'' must be within a callback registered to an event handled within the code provided as the ''FiberScheduler'' callback. Often it is desirable to ensure resumption of a fiber is asynchronous, making it easier to reason about program state before and after an event would resume a fiber. New fibers also must be started within a ''FiberScheduler'' (though the started fiber may use any scheduler within that fiber). +
- +
-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, or suspend to the prior fiber. In essense, a fiber may be pushed or popped from the stack, but execution cannot move to within the stack. The fiber scheduler acts as a hub which may branch into another fiber or suspend to the main fiber. If a fiber were to attempt to switch to a fiber that is already running, the program will crash. Checks prevent this from happening, throwing an exception into PHP instead of crashing the VM. +
- +
-If all references to an instance of ''FiberScheduler'' are destroyed before a script terminates, the fiber is treated like an [[#unfinished_fibers|unfinished fiber]]. +
- +
-When a script ends, each scheduler fiber that has not been previously destroyed or terminated is resumed in reverse order of creation to complete unfinished tasks or free resources.+
  
 ==== ReflectionFiber ==== ==== ReflectionFiber ====
  
-''ReflectionFiber'' is used to inspect executing fibers. A ''ReflectionFiber'' object can be created from any ''Fiber'' object, even if it has not been started or if it has been finished. This reflection class is similar to ''ReflectionGenerator''.+''ReflectionFiber'' is used to inspect executing fibers. A ''ReflectionFiber'' object can be created from any ''Fiber'' object, even if it has not been started or if it has terminated. This reflection class is similar to ''ReflectionGenerator''.
  
 <code php> <code php>
-class ReflectionFiber+final class ReflectionFiber
 { {
     /**     /**
Line 224: Line 170:
                          terminated.                          terminated.
      */      */
-    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(): string { }+    public function getExecutingFile(): string {}
  
     /**     /**
      * @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(): int { }+    public function getExecutingLine(): int {}
  
     /**     /**
Line 245: Line 192:
      * @return array Fiber backtrace, similar to {@see debug_backtrace()}      * @return array Fiber backtrace, similar to {@see debug_backtrace()}
                    and {@see ReflectionGenerator::getTrace()}.                    and {@see ReflectionGenerator::getTrace()}.
-     * 
-     * @throws ReflectionException If the fiber has not been started or has terminated. 
      */      */
-    public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): array { }+    public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): array {}
  
     /**     /**
      * @return bool True if the fiber has been started.      * @return bool True if the fiber has been started.
      */      */
-    public function isStarted(): bool { }+    public function isStarted(): bool {}
  
     /**     /**
      * @return bool True if the fiber is currently suspended.      * @return bool True if the fiber is currently suspended.
      */      */
-    public function isSuspended(): bool { }+    public function isSuspended(): bool {}
  
     /**     /**
      * @return bool True if the fiber is currently running.      * @return bool True if the fiber is currently running.
      */      */
-    public function isRunning(): bool { }+    public function isRunning(): bool {}
  
     /**     /**
      * @return bool True if the fiber has completed execution (either returning or      * @return bool True if the fiber has completed execution (either returning or
-                  throwing an exception).+                  throwing an exception), false otherwise.
      */      */
-    public function isTerminated(): bool { } +    public function isTerminated(): bool {}
-+
-</code> +
- +
-==== ReflectionFiberScheduler ==== +
- +
-''ReflectionFiberScheduler'' is used to inspect the internal fibers created from objects implementing ''FiberScheduler'' after being used to suspend a fiber. +
- +
-<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(): FiberScheduler { }+
 } }
 </code> </code>
Line 294: Line 220:
 === Unfinished Fibers === === Unfinished Fibers ===
  
-Fibers that are not finished (do not complete execution) are destroyed similarly to unfinished generators, executing any pending ''finally'' blocks. ''Fiber::suspend()'' may not be invoked in a force-closed fiber, just as ''yield'' cannot be used in a force-closed generator. Fibers are destroyed when there are no references to the ''Fiber'' object. An exception to this is the ''{main}'' fiber, where removing all references to the ''Fiber'' object that resumes the main fiber will result in a ''FiberExit'' exception to be thrown from the call to ''Fiber::suspend()'', resulting in a fatal error.+Fibers that are not finished (do not complete execution) are destroyed similarly to unfinished generators, executing any pending ''finally'' blocks. ''Fiber::suspend()'' may not be invoked in a force-closed fiber, just as ''yield'' cannot be used in a force-closed generator. Fibers are destroyed when there are no references to the ''Fiber'' object.
  
 === Fiber Stacks === === Fiber Stacks ===
  
-Each fiber is allocated a separate C stack and VM stack on the heap. The C stack is allocated using ''mmap'' if available, meaning physical memory is used only on demand (if it needs to be allocated to a stack value) on most platforms. Each fiber stack is allocated 1M maximum of memory by default, settable with an ini setting ''fiber.stack_size''. Note that this memory is used for the C stack and is not related to the memory available to PHP code. VM stacks for each fiber are allocated in a similar way to generators and use a similar amount of memory and CPU. VM stacks are able to grow dynamically, so only a single VM page (4K) is initially allocated.+Each fiber is allocated a separate C stack and VM stack on the heap. The C stack is allocated using ''mmap'' if available, meaning physical memory is used only on demand (if it needs to be allocated to a stack value) on most platforms. Each fiber stack is allocated maximum of 8M of memory by default, settable with an ini setting ''fiber.stack_size''. Note that this memory is used for the C stack and is not related to the memory available to PHP code. VM stacks for each fiber are allocated in a similar way to generators and use a similar amount of memory and CPU. VM stacks are able to grow dynamically, so only a single VM page (4K) is initially allocated.
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
  
-Declares ''Fiber'', ''FiberScheduler'', ''FiberError'', ''FiberExit'', ''ReflectionFiber'', and ''ReflectionFiberScheduler'' in the root namespace. No other BC breaks. +Declares ''Fiber'', ''FiberError'', ''FiberExit'', and ''ReflectionFiber'' in the root namespace. No other BC breaks.
- +
-===== 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, but an internal API would allow the extension to use the fiber implementation provided by PHP.
  
-''Fiber::suspend()'' could be replaced with a keyword defining a statement where the body of the statement is executed before suspending the fiber, setting the ''Fiber'' instance to the variable name given and entering the ''FiberScheduler'' instance from the expression. (''to'' should not need to be a keyword or reserved word, similar to ''from'' in ''yield from'').+===== Proposed PHP Version(s=====
  
-**suspend (** //variable// **to** //expression// **) {** //statement(s)// **}** +PHP 8.1
- +
-Below is an example usage of the proposed API inside an object method, where the current ''Fiber'' instance is added to an array before suspending, returning the value that resumes the fiber. +
- +
-<code php> +
-$this->waiting[$position] = Fiber::this(); +
-return Fiber::suspend($scheduler); +
-</code> +
- +
-The above could instead be written as the following using a ''suspend'' keyword. +
- +
-<code php> +
-return suspend ($fiber to $scheduler) { +
-    $this->waiting[$position] = $fiber; +
-+
-</code> +
- +
-=== async/await keywords === +
- +
-Using an internally defined ''FiberScheduler'' and an additionally defined ''Awaitable'' object, ''Fiber::suspend()'' could be replaced with the keyword ''await'' and new fibers could be created using the keyword ''async''. The usage of ''async'' differs slightly from languages such as JS or Hack. ''async'' is not used to declare asynchronous functions, rather it is used at call time to modify the call to any function or method to return an awaitable and start a new fiber. +
- +
-An ''Awaitable'' would act like a promise, representing the future result of the fiber created with ''async''+
- +
-<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. +
-</code> +
- +
-These keywords can be created in user code using the proposed fiber API. [[https://github.com/amphp/amp/tree/v3|amphp v3]] (a work-in-progress) defines ''[[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/functions.php#L6-L67|await() and async()]]'' functions to await ''Amp\Promise'' instances and create new coroutines. +
- +
-=== defer keyword === +
- +
-Fibers may be used to implement a ''defer'' keyword that executes a statement within a new fiber when the current fiber is suspended or terminates. Such a keyword would also require an internal implementation of ''FiberScheduler'' and likely would be an addition after async/await keywords. This behavior differs from ''defer'' in Go, as PHP is already able to mimick such behavior with ''finally'' blocks. +
- +
-This keyword also can be created in user code using the proposed fiber API, an example being ''[[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/functions.php#L83-L102|defer()]]'' in amphp v3.+
  
 ===== Proposed Voting Choices ===== ===== Proposed Voting Choices =====
Line 365: Line 252:
 ===== Examples ===== ===== Examples =====
  
-First let’s define a very simple scheduler class which will be used by our first examples to create a ''FiberScheduler'' to demonstrate how fibers are suspended and resumed. This scheduler is only able to defer a function to execute at a later time. These functions are executed in a loop within ''EventLoop->run()''.+This first simple example creates fiber that immediately suspends with the string ''"fiber"''. This string is returned from the call to ''$fiber->start()''. The fiber is then resumed with the string ''"test"'', which is returned from the call to ''Fiber::suspend()''.
  
 <code php> <code php>
-class EventLoop +$fiber new Fiber(function (): void { 
-+    $value = Fiber::suspend('fiber'); 
-    private array $callbacks []; +    echo "Value used to resume fiber: ", $value, "\n";
- +
-    /** +
-     * Run the scheduler. +
-     */ +
-    public function run(): void +
-    { +
-        while (!empty($this->callbacks)) { +
-            $callbacks = $this->callbacks; +
-            $this->callbacks = []; +
-            foreach ($callbacks as $id => $callback) { +
-                $callback(); +
-            } +
-        } +
-    } +
- +
-    /** +
-     * Enqueue a callback to executed at a later time. +
-     */ +
-    public function defer(callable $callback): void +
-    { +
-        $this->callbacks[] = $callback; +
-    +
-+
-</code> +
- +
-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> +
-$loop = new EventLoop; +
-$scheduler = new FiberScheduler(fn() => $loop->run()); +
- +
-// Get a reference to the currently executing fiber ({main} in this case). +
-$fiber = Fiber::this(); +
- +
-// 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 to resume the fiber at a later time. +
-$loop->defer(function () use ($fiber)void { +
-    // Fibers must be resumed within FiberScheduler. +
-    // This closure will be executed within the loop in EventLoop::run(). +
-    $fiber->resume("Test");+
 }); });
  
-// Suspend the main fiber, which will be resumed later by the scheduler. +$value = $fiber->start();
-$value = Fiber::suspend($scheduler);+
  
-echo "After resuming main fiber: ", $value, "\n"; // Output: After resuming main fiber: Test +echo "Value from fiber suspending: ", $value, "\n";
-</code>+
  
-This example is expanded for clarity and comments, but may be condensed using short closures. +$fiber->resume('test');
- +
-<code php> +
-$loop = new EventLoop; +
-$scheduler = new FiberScheduler(fn() => $loop->run()); +
- +
-$fiber = Fiber::this(); +
-$loop->defer(fn() => $fiber->resume("Test")); +
-$value = Fiber::suspend($scheduler); +
- +
-echo "After resuming main fiber: ", $value, "\n"; // Output: After resuming main fiber: Test+
 </code> </code>
  
-Fibers may also be resumed by throwing an exception.+This example will output the following:
  
-<code php+<code> 
-$loop = new EventLoop; +Value from fiber suspending: fiber 
-$scheduler = new FiberScheduler(fn() => $loop->run()); +Value used to resume fiber: test
- +
-// Get a reference to the currently executing fiber ({main} in this case). +
-$fiber = Fiber::this(); +
- +
-// 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. +
-$loop->defer(function () use ($fiber)void { +
-    $fiber->throw(new Exception("Test")); +
-}); +
- +
-try { +
-    // Suspend the main fiber, but this time it will be resumed with a thrown exception. +
-    $value = Fiber::suspend($scheduler); +
-    // The exception is thrown from the call to Fiber::suspend(). +
-} catch (Exception $exception) { +
-    echo $exception->getMessage(), "\n"; // Output: Test +
-}+
 </code> </code>
- 
-Again this example may be condensed using short closures. 
- 
-<code php> 
-$loop = new EventLoop; 
-$scheduler = new FiberScheduler(fn() => $loop->run()); 
- 
-$fiber = Fiber::this(); 
-$loop->defer(fn() => $fiber->throw(new Exception("Test"))); 
- 
-try { 
-    $value = Fiber::suspend($scheduler); 
-} catch (Exception $exception) { 
-    echo $exception->getMessage(), "\n"; // Output: Test 
-} 
-</code> 
- 
-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 adds to the event loop the ability to poll a socket for incoming data, invoking a callback when data becomes available on the socket. This scheduler can now be used to resume a fiber //only// when data becomes available on a socket, avoiding a blocking read.+The next example defines a very simple event loop with the ability to poll a socket for incoming data, invoking a callback when data becomes available on the socket. This event loop can now be used to resume a fiber //only// when data becomes available on a socket, avoiding a blocking read.
  
 <code php> <code php>
Line 541: Line 338:
  
 $loop = new EventLoop; $loop = new EventLoop;
-$scheduler = new FiberScheduler(fn() => $loop->run()); 
  
 // 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 "Waiting for data...\n";     echo "Waiting for data...\n";
  
     $fiber = Fiber::this();     $fiber = Fiber::this();
-    $scheduler->read($read, fn() => $fiber->resume()); +    $loop->read($read, fn() => $fiber->resume()); 
-    Fiber::suspend($scheduler);+    Fiber::suspend();
  
     $data = fread($read, 8192);     $data = fread($read, 8192);
Line 556: Line 352:
 }); });
  
-// Start the new fiber within the fiber scheduler+// Start the fiber, which will suspend while waiting for a read event
-$loop->defer(fn() => $fiber->start());+$fiber->start();
  
-// Suspend main fiber to enter the scheduler. +// Defer writing data to an event loop callback
-$fiber = Fiber::this(); +$loop->defer(fn() => fwrite($write, "Hello, world!"));
-$loop->defer(fn() => $fiber->resume("Writing data...\n")); +
-echo Fiber::suspend($scheduler);+
  
-// Write data in main thread once it is resumed+// Run the event loop
-fwrite($write, "Hello, world!");+$loop->run();
 </code> </code>
  
Line 572: Line 366:
 <code> <code>
 Waiting for data... Waiting for data...
-Writing data... 
 Received data: Hello, world! Received data: Hello, world!
 </code> </code>
Line 578: Line 371:
 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 ''fread()'' would block until data was available. 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 ''fread()'' would block until data was available.
  
-Below is a chart illustrating execution flow between the three fibers in this example, ''{main}'', the scheduler fiber, and the fiber created by ''new Fiber()''. Execution flow switches between fibers as ''Fiber::suspend()'' and ''Fiber->resume()'' are called or when a fiber terminates.+Below is a chart illustrating execution flow between ''{main}'' and the fiber created by ''new Fiber()''. Execution flow switches between fibers as ''Fiber::suspend()'' and ''Fiber->resume()'' are called or when a fiber terminates.
  
 {{ https://wiki.php.net/_media/rfc/fiber-flow.png?800 |Fiber execution flow}} {{ https://wiki.php.net/_media/rfc/fiber-flow.png?800 |Fiber execution flow}}
- 
----- 
- 
-The next example below uses ''[[https://github.com/amphp/ext-fiber/blob/67771864a376e58e47aa5ef6ef447a887b378d33/scripts/Loop.php|Loop]]'' from the ''ext-fiber'' tests, a simple event loop implemenation, yet more complex than that in the above examples, to delay execution of a function for 1000 milliseconds. When the fiber is suspended with ''Fiber::suspend()'', resumption of the fiber is scheduled with ''Loop->delay()'', which invokes the callback after the given number of milliseconds. An instance of ''FiberScheduler'' is returned from ''Loop::getScheduler()''. 
- 
-<code php> 
-$loop = new Loop; 
-$fiber = Fiber::this(); 
-$loop->delay(1000, fn() => $fiber->resume(42)); 
-var_dump(Fiber::suspend($loop->getScheduler())); // int(42) 
-</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 = new Fiber(function () use ($loop): void { 
-    $fiber = Fiber::this(); 
-    $loop->delay(1500, fn() => $fiber->resume(1)); 
-    $value = Fiber::suspend($loop->getScheduler()); 
-    var_dump($value); 
-}); 
-$loop->defer(fn() => $fiber->start()); 
- 
-$fiber = new Fiber(function () use ($loop): void { 
-    $fiber = Fiber::this(); 
-    $loop->delay(1000, fn() => $fiber->resume(2)); 
-    $value = Fiber::suspend($loop->getScheduler()); 
-    var_dump($value); 
-}); 
-$loop->defer(fn() => $fiber->start()); 
- 
-$fiber = new Fiber(function () use ($loop): void { 
-    $fiber = Fiber::this(); 
-    $loop->delay(2000, fn() => $fiber->resume(3)); 
-    $value = Fiber::suspend($loop->getScheduler()); 
-    var_dump($value); 
-}); 
-$loop->defer(fn() => $fiber->start()); 
- 
-// Suspend the main thread to enter the FiberScheduler. 
-$fiber = Fiber::this(); 
-$loop->delay(500, fn() => $fiber->resume(4)); 
-$value = Fiber::suspend($loop->getScheduler()); 
-var_dump($value); 
-</code> 
- 
-The above code will output the following: 
- 
-<code> 
-int(4) 
-int(2) 
-int(1) 
-int(3) 
-</code> 
- 
-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://github.com/amphp/ext-fiber/blob/67771864a376e58e47aa5ef6ef447a887b378d33/scripts/Loop.php|Loop]]'' from the prior example. Similar to one of the previous examples, this one reads and writes to a socket, but takes a slightly different approach, performing the reading and writing within the fiber scheduler in response to the sockets having data available or space to write. The scheduler then resumes the fiber with the data or the number of bytes written. The example also waits 1 second before writing to the socket to simulate network latency. 
- 
-<code php> 
-[$read, $write] = stream_socket_pair( 
-    stripos(PHP_OS, 'win') === 0 ? STREAM_PF_INET : STREAM_PF_UNIX, 
-    STREAM_SOCK_STREAM, 
-    STREAM_IPPROTO_IP 
-); 
- 
-// Set streams to non-blocking mode. 
-stream_set_blocking($read, false); 
-stream_set_blocking($write, false); 
- 
-$loop = new Loop; 
- 
-// Write data in a separate fiber after a 1 second delay. 
-$fiber = new Fiber(function () use ($loop, $write): void { 
-    $fiber = Fiber::this(); 
- 
-    // Suspend fiber for 1 second. 
-    echo "Waiting for 1 second...\n"; 
-    $loop->delay(1000, fn() => $fiber->resume()); 
-    Fiber::suspend($loop->getScheduler()); 
- 
-    // Write data to the socket once it is writable. 
-    echo "Writing data...\n"; 
-    $loop->write($write, 'Hello, world!', fn(int $bytes) => $fiber->resume($bytes)); 
-    $bytes = Fiber::suspend($loop->getScheduler()); 
- 
-    echo "Wrote {$bytes} bytes.\n"; 
-}); 
- 
-$loop->defer(fn() => $fiber->start()); 
- 
-echo "Waiting for data...\n"; 
- 
-// Read data in main fiber. 
-$fiber = Fiber::this(); 
-$loop->read($read, fn(?string $data) => $fiber->resume($data)); 
-$data = Fiber::suspend($loop->getScheduler()); 
- 
-echo "Received data: ", $data, "\n"; 
-</code> 
- 
-The above code will output the following: 
- 
-<code> 
-Waiting for data... 
-Waiting for 1 second... 
-Writing data... 
-Wrote 13 bytes. 
-Received data: Hello, world! 
-</code> 
- 
-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. 
- 
  
 ---- ----
Line 706: Line 379:
 The next few examples use the async framework [[https://github.com/amphp/amp/tree/v3|amphp v3]] mentioned in [[#patches_and_tests|Patches and Tests]] to demonstrate how fibers may be used by frameworks to create asynchronous code that is written like synchronous code. The next few examples use the async framework [[https://github.com/amphp/amp/tree/v3|amphp v3]] mentioned in [[#patches_and_tests|Patches and Tests]] to demonstrate how fibers may be used by frameworks to create asynchronous code that is written like synchronous code.
  
-amphp v3 uses an [[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/Loop/Driver.php|event loop interface]] that extends ''FiberScheduler'' together with a variety of functions and a placeholder object (''[[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/Promise.php|Promise]]'') to build on top of the underlying fiber API to create its own opinionated API to create green-threads (coroutines) to execute code concurrently. Users of amphp v3 do not use the Fiber API directly, the framework handles suspending and creating fibers as necessary. Other frameworks may choose to approach creating green-threads and placeholders differently+amphp v3 uses an [[https://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/Loop/Driver.php|event loop interface]] together with a variety of functions and a placeholder object (''[[https://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/Promise.php|Promise]]'') to build on top of the underlying fiber API to create its own opinionated API to create green-threads (coroutines) to execute code concurrently. Users of amphp v3 do not use the Fiber API directly, the framework handles suspending and creating fibers as necessary, including adding the ability to await from ''{main}}''. Other frameworks may choose to approach creating green-threads and placeholders differently.
- +
-The ''[[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/functions.php#L83-L102|defer(callable $callback, mixed ...$args)]]'' function creates a new fiber that is executed when the current fiber suspends or terminates. ''[[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/functions.php#L220-L230|delay(int $milliseconds)]]'' suspends the current fiber until the given number of milliseconds has elasped.+
  
-This example is similar to the example above which created mutiple fibers that “slept” for differing timesbut 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 ''[[https://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/functions.php#L136-L155|defer(callable $callbackmixed ...$args)]]'' function creates a new fiber that is executed when the current fiber suspends or terminates''[[https://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/functions.php#L273-L281|delay(int $milliseconds)]]'' suspends the current fiber until the given number of milliseconds has elasped.
  
 <code php> <code php>
Line 733: Line 404:
 }); });
  
-// Suspend the main fiber with delay().+// Suspend the main context with delay().
 delay(500); delay(500);
 var_dump(4); var_dump(4);
 </code> </code>
- 
  
 ---- ----
  
-The next example again uses amphp v3 to demonstrate how the ''FiberScheduler'' fiber continues executing while the main thread is suspended. The ''[[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/functions.php#L6-L38|await(Promise $promise)]]'' function suspends a fiber until the given promise is resolved and the ''[[https://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/functions.php#L40-L67|async(callable $callback, mixed ...$args)]]'' function creates a new fiber, returning a promise that is resolved when the fiber completes, allowing multiple fibers to be executed concurrently.+The next example again uses amphp v3 to demonstrate how the event loop fiber continues executing while the main thread is "suspended". The ''[[https://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/functions.php#L6-L91|await(Promise $promise)]]'' function suspends a fiber until the given promise is resolved and the ''[[https://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/functions.php#L93-L120|async(callable $callback, mixed ...$args)]]'' function creates a new fiber, returning a promise that is resolved when the fiber completes, allowing multiple fibers to be executed concurrently.
  
 <code php> <code php>
Line 773: 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([  // Executed simultaneously, only 1 second will elapse during this await. $result = await([  // Executed simultaneously, only 1 second will elapse during this await.
-    async('asyncTask', 2), // async() creates a new fiber and returns a promise for the result. +    async(fn() => asyncTask(2)), // async() creates a new fiber and returns a promise for the result. 
-    async('asyncTask', 3),+    async(fn() => asyncTask(3)),
 ]); ]);
 var_dump($result); // Executed after 2 seconds. var_dump($result); // Executed after 2 seconds.
Line 783: 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);
  
 $running = false; // Stop the loop in the fiber created with defer() above. $running = false; // Stop the loop in the fiber created with defer() above.
 </code> </code>
- 
  
 ---- ----
  
-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://github.com/amphp/amp/blob/80ea42bdcfc4176f78162d5e3cbcad9c4cc20761/lib/Delayed.php|Delayed]]'', a promise-like object that resolves itself with the second argument after the number of milliseconds given as the first argument. When iterating over the generator, the ''foreach'' loop will suspend while waiting for another value to be yielded from the generator.+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://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/Delayed.php|Delayed]]'', a promise-like object that resolves itself with the second argument after the number of milliseconds given as the first argument. When iterating over the generator, the ''foreach'' loop will suspend while waiting for another value to be yielded from the generator.
  
 <code php> <code php>
Line 820: Line 489:
 var_dump(...generator()); var_dump(...generator());
 </code> </code>
- 
  
 ---- ----
  
-The example below shows how [[https://github.com/reactphp|ReactPHP]] might use fibers to define an ''await()'' function using their ''PromiseInterface'' and ''LoopInterface''(Note this example assumes ''LoopInterface'' would add a ''getScheduler()'' method returning an instance of ''FiberScheduler'' associated with the event loop instance.)+The example below shows how [[https://github.com/reactphp|ReactPHP]] might use fibers to define an ''await()'' function that could be used to await promise resolution within a fiber using their ''PromiseInterface'' and ''LoopInterface''.
  
 <code php> <code php>
Line 833: Line 501:
 { {
     $fiber = Fiber::this();     $fiber = Fiber::this();
 +    if ($fiber === null) {
 +        throw new Error('Promises can only be awaited within a fiber');
 +    }
  
     $promise->done(     $promise->done(
Line 839: Line 510:
     );     );
  
-    return Fiber::suspend($loop->getScheduler());+    return Fiber::suspend();
 } }
 </code> </code>
  
 A demonstration of integrating ReactPHP with fibers has been implemented in [[https://github.com/trowski/react-fiber|trowski/react-fiber]] for the current stable versions of ''react/event-loop'' and ''react/promise''. A demonstration of integrating ReactPHP with fibers has been implemented in [[https://github.com/trowski/react-fiber|trowski/react-fiber]] for the current stable versions of ''react/event-loop'' and ''react/promise''.
- 
- 
----- 
- 
-The final example uses the ''cURL'' extension to create a fiber scheduler based on ''curl_multi_exec()'' to perform multiple HTTP requests concurrently. When a new fiber is started, ''Scheduler->async()'' returns a ''Promise'', a placeholder to represent the eventual result of the new fiber, which may be awaited later using ''Scheduler->await()''. 
- 
-<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->scheduler = $scheduler; 
-    } 
- 
-    public function await(): mixed 
-    { 
-        if (!$this->resolved) { 
-            $this->schedule(Fiber::this()); 
-            return Fiber::suspend($this->scheduler->getScheduler()); 
-        } 
- 
-        if ($this->error) { 
-            throw $this->error; 
-        } 
- 
-        return $this->result; 
-    } 
- 
-    public function schedule(Fiber $fiber): void 
-    { 
-        if ($this->resolved) { 
-            if ($this->error !== null) { 
-                $this->scheduler->defer(fn() => $fiber->throw($this->error)); 
-            } else { 
-                $this->scheduler->defer(fn() => $fiber->resume($this->result)); 
-            } 
- 
-            return; 
-        } 
- 
-        $this->fibers[] = $fiber; 
-    } 
- 
-    public function resolve(mixed $value = null): void 
-    { 
-        if ($this->resolved) { 
-            throw new Error("Promise already resolved"); 
-        } 
- 
-        $this->result = $value; 
-        $this->continue(); 
-    } 
- 
-    public function fail(Throwable $error): void 
-    { 
-        if ($this->resolved) { 
-            throw new Error("Promise already resolved"); 
-        } 
- 
-        $this->error = $error; 
-        $this->continue(); 
-    } 
- 
-    private function continue(): void 
-    { 
-        $this->resolved = true; 
- 
-        $fibers = $this->fibers; 
-        $this->fibers = []; 
- 
-        foreach ($fibers as $fiber) { 
-            $this->schedule($fiber); 
-        } 
-    } 
-} 
- 
-class Scheduler 
-{ 
-    private FiberScheduler $scheduler; 
-    /** @var resource */ 
-    private $curl; 
-    /** @var callable[] */ 
-    private array $defers = []; 
-    /** @var Fiber[] */ 
-    private array $fibers = []; 
- 
-    public function __construct() 
-    { 
-        $this->curl = curl_multi_init(); 
-    } 
- 
-    public function __destruct() 
-    { 
-        curl_multi_close($this->curl); 
-    } 
- 
-    public function getScheduler(): FiberScheduler 
-    { 
-        if (!isset($this->scheduler) || $this->scheduler->isTerminated()) { 
-            $this->scheduler = new FiberScheduler(fn() => $this->run()); 
-        } 
- 
-        return $this->scheduler; 
-    } 
- 
-    public function fetch(string $url): string 
-    { 
-        $curl = curl_init(); 
- 
-        curl_setopt($curl, CURLOPT_URL, $url); 
-        curl_setopt($curl, CURLOPT_HEADER, 0); 
-        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 
- 
-        curl_multi_add_handle($this->curl, $curl); 
- 
-        $this->fibers[(int) $curl] = Fiber::this(); 
-        Fiber::suspend($this->getScheduler()); 
- 
-        $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); 
-        if ($status !== 200) { 
-            throw new Exception(sprintf('Request to %s failed with status code %d', $url, $status)); 
-        } 
- 
-        $body = substr(trim(curl_multi_getcontent($curl)), 0, 255); 
- 
-        curl_close($curl); 
- 
-        return $body; 
-    } 
- 
-    public function defer(callable $callable): void 
-    { 
-        $this->defers[] = $callable; 
-    } 
- 
-    public function async(callable $callable): Promise 
-    { 
-        $promise = new Promise($this); 
- 
-        $fiber = new Fiber(function () use ($promise, $callable) { 
-            try { 
-                $promise->resolve($callable()); 
-            } catch (Throwable $e) { 
-                $promise->fail($e); 
-            } 
-        }); 
- 
-        $this->defer(fn() => $fiber->start()); 
- 
-        return $promise; 
-    } 
- 
-    public function run(): void 
-    { 
-        do { 
-            do { 
-                $defers = $this->defers; 
-                $this->defers = []; 
- 
-                foreach ($defers as $callable) { 
-                    $callable(); 
-                } 
- 
-                $status = curl_multi_exec($this->curl, $active); 
-                if ($active) { 
-                    $select = curl_multi_select($this->curl); 
-                    if ($select > 0) { 
-                        $this->processQueue(); 
-                    } 
-                } 
-            } while ($active && $status === CURLM_OK); 
- 
-            $this->processQueue(); 
-        } while ($this->defers); 
-    } 
- 
-    private function processQueue(): void 
-    { 
-        while ($info = curl_multi_info_read($this->curl)) { 
-            if ($info['msg'] !== CURLMSG_DONE) { 
-                continue; 
-            } 
- 
-            $fiber = $this->fibers[(int) $info['handle']]; 
-            $fiber->resume(); 
-        } 
-    } 
-} 
- 
-function await(Promise ...$promises): array 
-{ 
-    return array_map(fn($promise) => $promise->await(), $promises); 
-} 
- 
-$urls = array_fill(0, $argv[1] ?? 10, 'https://amphp.org/'); 
- 
-$scheduler = new Scheduler; 
- 
-$promises = []; 
-foreach ($urls as $url) { 
-    $promises[] = $scheduler->async(fn() => $scheduler->fetch($url)); 
-} 
- 
-print 'Starting to make ' . count($promises) . ' requests...' . PHP_EOL; 
- 
-$start = hrtime(true); 
- 
-$responses = await(...$promises); 
- 
-// var_dump($responses); 
- 
-print ((hrtime(true) - $start) / 1_000_000) . 'ms' . PHP_EOL; 
-</code> 
  
 ===== FAQ ===== ===== FAQ =====
Line 1074: Line 525:
  
 ''FFI'' is an example of a feature recently added to PHP that most users may not use directly, but can benefit from greatly within libraries they use. ''FFI'' is an example of a feature recently added to PHP that most users may not use directly, but can benefit from greatly within libraries they use.
- 
-=== 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 ''foreach'', you are using a “scheduler” to control the generator. If you write code using the ''send()'' or ''throw()'' methods of a generator, you are writing a generator scheduler. However, because generators are stack-less and can only yield from their immediate context, the author of a generator has direct control over what is yielded within that generator. 
- 
-**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, using a fiber scheduler API enables a few features: 
- 
-  * Suspension of the top-level (''{main}''): When the main fiber is suspended, execution continues into the fiber scheduler. 
-  * Nesting schedulers: A fiber may suspend into different fiber schedulers at various suspension points. Each scheduler will be started/suspended/resumed as needed. While a fully asynchronous app may want to ensure it does not use multiple fiber schedulers, a FPM application may find it acceptable to do so. 
-  * Elimination of boilerplate: Suspending at the top-level eliminates the need for an application to wrap code into a library-specific scheduler, allowing library code to suspend and resume as needed, without concern that the user used the appropriate scheduler that may conflict with another library’s scheduler. 
- 
-**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, and require more work and complexity for the average developer to use any library using fibers.** 
  
 === What about performance? === === What about performance? ===
Line 1104: Line 541:
  
 Functions such as ''debug_backtrace()'' and exception backtraces only include the trace of the current fiber. Previous fiber backtraces are not included currently, though this may be possible with some modification to the internal functions that generate these traces to also include the backtrace of the fibers that entered the current fiber. Functions such as ''debug_backtrace()'' and exception backtraces only include the trace of the current fiber. Previous fiber backtraces are not included currently, though this may be possible with some modification to the internal functions that generate these traces to also include the backtrace of the fibers that entered the current fiber.
- 
-=== How is a FiberScheduler callback implemented? === 
- 
-A ''FiberScheduler'' callback should invoke the necessary code to resume suspended fibers for the given application. Generally, a ''FiberScheduler'' would be created from a function that runs a loop through available events, resuming fibers when an event occurs. The ''ext-fiber'' repo contains a very simple implementation, ''[[https://github.com/amphp/ext-fiber/blob/67771864a376e58e47aa5ef6ef447a887b378d33/scripts/Loop.php|Loop]]'', that is able to delay functions for a given number of milliseconds or until the scheduler is entered again. This simple implementation is used in the [[https://github.com/amphp/ext-fiber/tree/67771864a376e58e47aa5ef6ef447a887b378d33/tests|ext-fiber tests]]. 
  
 === How does blocking code affect fibers === === How does blocking code affect fibers ===
Line 1113: Line 546:
 Blocking code (such as ''file_get_contents()'') will continue to block the entire process, even if other fibers exist. Code must be written to use asynchonous I/O, an event loop, and fibers to see a performance and concurrency benefit. As mentioned in the introduction, several libraries already exist for asynchronous I/O and can take advantage of fibers to integrate with synchronous code while expanding the potential for concurrency in an application. Blocking code (such as ''file_get_contents()'') will continue to block the entire process, even if other fibers exist. Code must be written to use asynchonous I/O, an event loop, and fibers to see a performance and concurrency benefit. As mentioned in the introduction, several libraries already exist for asynchronous I/O and can take advantage of fibers to integrate with synchronous code while expanding the potential for concurrency in an application.
  
-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 scheduler is available in the future, internal functions such as ''sleep()'' could be made non-blocking by default.+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 ''sleep()'' could be made non-blocking by default.
  
 === How do various fibers access the same memory? === === How do various fibers access the same memory? ===
Line 1119: 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 ''[[https://github.com/amphp/ext-fiber/blob/67771864a376e58e47aa5ef6ef447a887b378d33/scripts/Loop.php|Loop]]'' implementation in the [[https://github.com/amphp/ext-fiber/tree/67771864a376e58e47aa5ef6ef447a887b378d33/tests|ext-fiber tests]]. +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->loop = $loop; +
-        $this->queue = new SplQueue; +
-    } +
- +
-    public function acquire(): Lock +
-    { +
-        if ($this->locked) { +
-            $this->queue->push(Fiber::this()); +
-            Fiber::suspend($this->loop->getScheduler()); +
-        } +
- +
-        $this->locked = true; +
- +
-        return new Lock(function (): void { +
-            if ($this->queue->isEmpty()) { +
-                $this->locked = false; +
-                return; +
-            } +
- +
-            $fiber = $this->queue->pop(); +
-            $this->loop->defer(fn() => $fiber->resume()); +
-        }); +
-    } +
-+
- +
-class Lock +
-+
-    private \Closure $release; +
- +
-    public function __construct(\Closure $release) +
-    { +
-        $this->release = $release; +
-    } +
- +
-    public function __destruct() +
-    { +
-        $this->release(); +
-    } +
- +
-    public function release(): void +
-    { +
-        if (isset($this->release)) { +
-            $release = $this->release; +
-            unset($this->release); +
-            ($release)(); +
-        } +
-    } +
-+
-</code>+
  
 === Why add this to PHP core? === === Why add this to PHP core? ===
Line 1192: 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 [[#future_scope|Future Scope]].+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://wiki.php.net/rfc/fiber|Fiber RFC]] did not support context switching within internal calls (''array_map'', ''preg_replace_callback'', etc.) or opcode handlers (''foreach'', ''yield from'', etc.). This could result in a crash if a function using fibers was used in any user code called from C code or in extensions that override ''zend_execute_ex'' such as Xdebug. The prior [[https://wiki.php.net/rfc/fiber|Fiber RFC]] did not support context switching within internal calls (''array_map'', ''preg_replace_callback'', etc.) or opcode handlers (''foreach'', ''yield from'', etc.). This could result in a crash if a function using fibers was used in any user code called from C code or in extensions that override ''zend_execute_ex'' such as Xdebug.
- 
-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 1211: Line 583:
  
 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="Add Fibers to PHP?" auth="trowski" voteType="single" closed="true">
 +   * Yes
 +   * No
 +</doodle>
  
 ===== References ===== ===== References =====
rfc/fibers.1612298676.txt.gz · Last modified: 2021/02/02 20:44 by kelunik