rfc:fibers

Differences

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

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:fibers [2021/02/12 20:41] trowskirfc: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:
Line 27: Line 27:
   * Adding a ''Fiber'' class and the corresponding reflection class ''ReflectionFiber''.   * Adding a ''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.
 +
 +Fibers allow for transparent non-blocking I/O implementations of existing interfaces (such as PSR-7, Doctine ORM, etc.). This is because the placeholder (promise) object is eliminated. Functions instead can declare the I/O result type instead of a placeholder object which cannot specify a resolution type because PHP does not support generics.
  
 ==== Fibers ==== ==== Fibers ====
Line 47: Line 49:
  
 A Fiber would be represented as class which would be defined in core PHP with the following signature: A Fiber would be represented as class which would be defined in core PHP with the following signature:
 +
 +<blockquote>Fiber::this() has been renamed to Fiber::getCurrent() during the PHP 8.1 alpha release phase.</blockquote>
  
 <code php> <code php>
Line 54: Line 58:
      * @param callable $callback Function to invoke when starting the fiber.      * @param callable $callback Function to invoke when starting the fiber.
      */      */
-    public function __construct(callable $callback) { }+    public function __construct(callable $callback) {}
  
     /**     /**
Line 61: Line 65:
      * @param mixed ...$args Arguments passed to fiber function.      * @param mixed ...$args Arguments passed to fiber function.
      *      *
-     * @return mixed Value from the first suspension point.+     * @return mixed Value from the first suspension point or NULL if the fiber returns.
      *      *
-     * @throw FiberError If the fiber is running or terminated.+     * @throw FiberError If the fiber has already been started.
      * @throw Throwable If the fiber callable throws an uncaught exception.      * @throw Throwable If the fiber callable throws an uncaught exception.
      */      */
-    public function start(mixed ...$args): mixed { }+    public function start(mixed ...$args): mixed {}
  
     /**     /**
Line 74: Line 78:
      * @param mixed $value      * @param mixed $value
      *      *
-     * @return mixed Value from the next suspension point or NULL if the fiber terminates.+     * @return mixed Value from the next suspension point or NULL if the fiber returns.
      *      *
-     * @throw FiberError If the fiber is running or terminated.+     * @throw FiberError If the fiber has not started, 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): mixed { }+    public function resume(mixed $value = null): mixed {}
  
     /**     /**
Line 87: Line 91:
      * @param Throwable $exception      * @param Throwable $exception
      *      *
-     * @return mixed Value from the next suspension point or NULL if the fiber terminates.+     * @return mixed Value from the next suspension point or NULL if the fiber returns.
      *      *
-     * @throw FiberError If the fiber is running or terminated.+     * @throw FiberError If the fiber has not started, 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): mixed { }+    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 {}
  
     /**     /**
-     * @return mixed Return value of the fiber callback.+     * @return mixed Return value of the fiber callback. NULL is returned if the fiber does not have a return statement.
      *      *
-     * @throws FiberError If the fiber has not terminated or did not return a value.+     * @throws FiberError If the fiber has not terminated or the fiber threw an exception.
      */      */
-    public function getReturn(): mixed { }+    public function getReturn(): mixed {}
  
     /**     /**
      * @return self|null Returns the currently executing fiber instance or NULL if in {main}.      * @return self|null Returns the currently executing fiber instance or NULL if in {main}.
      */      */
-    public static function this(): ?self { }+    public static function this(): ?self {}
  
     /**     /**
Line 135: Line 139:
      * @return mixed Value provided to {@see Fiber::resume()}.      * @return mixed Value provided to {@see Fiber::resume()}.
      *      *
 +     * @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(mixed $value = null): 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 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 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.
  
 A suspended fiber may be resumed in one of two ways: A suspended fiber may be resumed in one of two ways:
Line 148: Line 155:
   * throwing an exception from ''Fiber::suspend()'' using ''Fiber->throw()''   * throwing an exception from ''Fiber::suspend()'' using ''Fiber->throw()''
  
-''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->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.
  
-''Fiber::this()'' returns the currently executing ''Fiber'' instance. This allows a fiber to store a reference to itself elsewhere, such as within an event loop callback or an array of awaiting fibers.+''Fiber::this()'' returns the currently executing ''Fiber'' instance or ''NULL'' if called from ''{main}''. This allows a fiber to store a reference to itself elsewhere, such as within an event loop callback or an array of awaiting fibers.
  
 ==== 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 163: 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 184: 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> </code>
Line 214: Line 220:
 === Unfinished Fibers === === Unfinished Fibers ===
  
-Fibers that are not finished (do not complete execution) are destroyed similarly to unfinished generators, executing any pending ''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'', ''FiberError'', ''FiberExit'', and ''ReflectionFiber'' 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 ''suspend'' that would be similar to ''yield''. The keyword statement would accept an optional expression that would be the suspend value and return the value used to resume the fiber.+===== Proposed PHP Version(s=====
  
-//variable// = **suspend** //optional-expression//**;** +PHP 8.1
- +
-=== async/await keywords === +
- +
-Using an internally defined event loop (or fiber scheduler) and an additionally defined ''Awaitable'' object. Fibers (green-threads) would be suspended using an ''Awaitable'' and the keyword ''await''. New green-threads 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 an event loop 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 397: 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]] 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, including adding the ability to await from ''{main}}''. 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.+The ''[[https://github.com/amphp/amp/blob/b0b9489a2cd25f33a8dafc05b3ad3594a5b66627/lib/functions.php#L136-L155|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/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 429: Line 411:
 ---- ----
  
-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/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 461: Line 443:
 // await() suspends the fiber until the given promise (or array of promises here) are resolved. // await() suspends the fiber until the given promise (or array of promises here) are resolved.
 $result = await([  // 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 471: Line 453:
 // array_map() takes 2 seconds to execute as the two calls are not concurrent, but this shows // array_map() takes 2 seconds to execute as the two calls are not concurrent, but this shows
 // that fibers are supported by internal callbacks. // that fibers are supported by internal callbacks.
-$result = array_map('asyncTask', [5, 6]);+$result = array_map(fn(int $value) => asyncTask($value), [5, 6]);
 var_dump($result); var_dump($result);
  
Line 479: Line 461:
 ---- ----
  
-Since fibers can be paused during calls within the PHP VM, fibers can also be used to create asynchronous iterators and generators. The example below uses amphp v3 to suspend a fiber within a generator, awaiting resolution of a ''[[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 519: 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 567: Line 552:
 All fibers exist within a single thread. Only a single fiber may execute at a time, so memory cannot be accessed or modified simultaneously by multiple fibers, unlike threads which may modify memory simultaneously. All fibers exist within a single thread. Only a single fiber may execute at a time, so memory cannot be accessed or modified simultaneously by multiple fibers, unlike threads which may modify memory simultaneously.
  
-As fibers are suspended and resumed, execution of multiple fibers that access the same memory can be interleaved. Thus a running fiber may modify memory depended upon by another suspended fiber. There are various strategies to address this problem, including mutexes, semaphores, memory parcels, and channels. This RFC does not provide any such implementations as these can be implemented in user space code using the proposed fiber API. Below is an example implementation of a mutex using the proposed fiber API and the example ''[[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->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 640: Line 566:
 It is the opinion of the authors of this RFC that it is best to provide the bare minimum in core and allow user code to implement other components as they desire. If the community moves toward a single event loop API or a need emerges for an event loop in PHP core, this can be done in a future RFC. Providing a core event loop without core functionality using it (such as streams, file access, etc.) would be misleading and confusing for users. Deferring such functionality to user frameworks and providing only a minimum API in core keeps expectations in check. It is the opinion of the authors of this RFC that it is best to provide the bare minimum in core and allow user code to implement other components as they desire. If the community moves toward a single event loop API or a need emerges for an event loop in PHP core, this can be done in a future RFC. Providing a core event loop without core functionality using it (such as streams, file access, etc.) would be misleading and confusing for users. Deferring such functionality to user frameworks and providing only a minimum API in core keeps expectations in check.
  
-This RFC does not preclude adding async/await and an event loop to core, see [[#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? ===
Line 657: 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.1613162497.txt.gz · Last modified: 2021/02/12 20:41 by trowski