rfc:fibers
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionLast revisionBoth sides next revision | ||
rfc:fibers [2020/10/31 14:37] – trowski | rfc:fibers [2021/04/26 20:43] – trowski | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== PHP RFC: Fibers ====== | ====== PHP RFC: Fibers ====== | ||
- | * Version: 0.1 | + | |
- | * Date: 2020-09-04 | + | * Date: 2021-03-08 |
- | * Author: Aaron Piotrowski < | + | * Authors: Aaron Piotrowski <trowski@php.net>, |
- | * Status: | + | * Status: |
* First Published at: http:// | * First Published at: http:// | ||
===== Introduction ===== | ===== Introduction ===== | ||
- | Fibers create full-stack, interruptible functions that may be used to implement cooperative concurrency in PHP. These are also know as coroutines or green-threads. Unlike stack-less Generators, each Fiber contains a call stack, allowing them to be paused within deeply nested function calls. A function declaring an interruption point (i.e., calling '' | ||
- | Implementations | + | 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 |
+ | |||
+ | More recently, there have been multiple projects that have allowed people to write asynchronous PHP code to allow for concurrent I/O operations. Asynchronous functions accept a callback or return a placeholder for a future value (such as a promise) to run code at a future time once the result is available. Execution continues without waiting for a result. Examples of these projects are [[https:// | ||
+ | |||
+ | The problem this RFC seeks to address is a difficult one to explain, but can be referred to as the [[https:// | ||
+ | |||
+ | A summary of the problem described in the linked article is: | ||
+ | |||
+ | * 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). | ||
+ | * Calling an asynchronous function requires the entire call stack to be asynchronous | ||
+ | |||
+ | 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 | ||
+ | |||
+ | This RFC seeks to eliminate the distinction between synchronous and asynchronous functions by allowing functions to be interruptible without polluting the entire call stack. This would be achieved by: | ||
+ | |||
+ | * Adding support for [[https:// | ||
+ | * Adding a '' | ||
+ | * Adding exception classes '' | ||
+ | |||
+ | Fibers allow for transparent non-blocking I/O implementations of existing interfaces (such as PSR-7, Doctine ORM, etc.). This is because the placeholder (promise) object is eliminated. Functions instead can declare the I/O result type instead of a placeholder object which cannot specify a resolution type because PHP does not support generics. | ||
+ | |||
+ | ==== Fibers ==== | ||
+ | |||
+ | Fibers 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 pause the entire execution stack, so the direct caller of the function does not need to change how it invokes the function. | ||
+ | |||
+ | Execution may be interrupted anywhere in the call stack using '' | ||
+ | |||
+ | 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 '' | ||
+ | |||
+ | Fibers can be suspended in //any// function call, including those called from within the PHP VM, such as functions provided to '' | ||
- | Fibers aim to solve this problem, allowing functions to be interruptible without polluting | + | Once suspended, execution of the fiber may be resumed with any value using '' |
===== Proposal ===== | ===== Proposal ===== | ||
- | **This RFC proposes adding components to PHP implementing interruptible fibers and top-level (main-thread) await.** | ||
- | A fiber is created with any callable and variadic argument list using '' | + | ==== Fiber ==== |
- | === Fiber === | + | A Fiber would be represented as class which would be defined in core PHP with the following signature: |
<code php> | <code php> | ||
Line 24: | Line 54: | ||
{ | { | ||
/** | /** | ||
- | | + | * @param callable $callback Function to invoke when starting the fiber. |
+ | */ | ||
+ | public function __construct(callable $callback) {} | ||
+ | |||
+ | /** | ||
+ | * Starts execution of the fiber. Returns when the fiber suspends or terminates. | ||
+ | * | ||
+ | * @param mixed ...$args Arguments passed to fiber function. | ||
+ | * | ||
+ | * @return mixed Value from the first suspension point or NULL if the fiber returns. | ||
* | * | ||
- | * @param callable $callback Function to invoke when starting | + | * @throw FiberError If the fiber has already been started. |
- | * @param mixed ...$args Function arguments. | + | * @throw Throwable If the fiber callable throws an uncaught exception. |
*/ | */ | ||
- | public | + | public function |
/** | /** | ||
- | | + | |
+ | * Returns when the fiber suspends or terminates. | ||
+ | * | ||
+ | * @param mixed $value | ||
+ | * | ||
+ | * @return mixed Value from the next suspension point or NULL if the fiber returns. | ||
+ | * | ||
+ | * @throw FiberError If the fiber has not started, is running, or has terminated. | ||
+ | * @throw Throwable If the fiber callable throws an uncaught exception. | ||
*/ | */ | ||
- | | + | |
/** | /** | ||
- | | + | |
+ | * Returns when the fiber suspends or terminates. | ||
* | * | ||
- | * @param | + | * @param |
- | * @param FiberScheduler $scheduler | + | |
* | * | ||
- | * @return mixed Resolution value of the awaitable. | + | * @return mixed Value from the next suspension point or NULL if the fiber returns. |
* | * | ||
- | * @throws | + | * @throw FiberError |
- | * @throws | + | * @throw Throwable If the fiber callable |
*/ | */ | ||
- | public | + | public function |
- | } | + | |
- | </ | + | |
- | '' | + | /** |
+ | * @return bool True if the fiber has been started. | ||
+ | */ | ||
+ | public function isStarted(): bool {} | ||
- | === Awaitable === | + | /** |
+ | * @return bool True if the fiber is suspended. | ||
+ | */ | ||
+ | public function isSuspended(): | ||
+ | |||
+ | /** | ||
+ | * @return bool True if the fiber is currently running. | ||
+ | */ | ||
+ | public function isRunning(): | ||
+ | |||
+ | /** | ||
+ | * @return bool True if the fiber has completed execution (returned or threw). | ||
+ | */ | ||
+ | public function isTerminated(): | ||
- | <code php> | ||
- | interface Awaitable | ||
- | { | ||
/** | /** | ||
- | | + | |
* | * | ||
- | * @param callable(? | + | * @throws FiberError If the fiber has not terminated or the fiber threw an exception. |
*/ | */ | ||
- | public function | + | public function |
+ | |||
+ | /** | ||
+ | * @return self|null Returns the currently executing fiber instance or NULL if in {main}. | ||
+ | */ | ||
+ | public static function this(): ?self {} | ||
+ | |||
+ | /** | ||
+ | * Suspend execution of the fiber. The fiber may be resumed with {@see Fiber:: | ||
+ | * | ||
+ | * Cannot be called from {main}. | ||
+ | * | ||
+ | * @param mixed $value Value to return from {@see Fiber:: | ||
+ | * | ||
+ | * @return mixed Value provided to {@see Fiber:: | ||
+ | * | ||
+ | * @throws FiberError Thrown if not within a fiber (i.e., if called from {main}). | ||
+ | * @throws Throwable Exception provided to {@see Fiber:: | ||
+ | */ | ||
+ | public static function suspend(mixed | ||
} | } | ||
</ | </ | ||
- | An awaitable | + | A '' |
+ | |||
+ | '' | ||
+ | |||
+ | A suspended fiber may be resumed in one of two ways: | ||
+ | |||
+ | * returning a value from '' | ||
+ | * throwing an exception from '' | ||
+ | |||
+ | '' | ||
+ | |||
+ | '' | ||
- | When an '' | + | ==== ReflectionFiber ==== |
- | === FiberScheduler === | + | '' |
<code php> | <code php> | ||
- | interface FiberScheduler | + | final class ReflectionFiber |
{ | { | ||
/** | /** | ||
- | | + | |
+ | | ||
*/ | */ | ||
- | public function | + | public function |
- | } | + | |
- | </ | + | |
- | A '' | + | /** |
+ | * @return | ||
+ | */ | ||
+ | public function getFiber(): Fiber {} | ||
- | When an instance | + | /** |
+ | * @return string Current file of fiber execution. | ||
+ | */ | ||
+ | public function getExecutingFile(): string {} | ||
- | '' | + | /** |
+ | * @return int Current line of fiber execution. | ||
+ | */ | ||
+ | public function getExecutingLine(): int {} | ||
- | A fiber //must// be resumed from the fiber created from the instance of '' | + | |
+ | * @param int $options Same flags as {@see debug_backtrace()}. | ||
+ | * | ||
+ | * @return array Fiber backtrace, similar to {@see debug_backtrace()} | ||
+ | | ||
+ | */ | ||
+ | public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): | ||
+ | |||
+ | /** | ||
+ | * @return bool True if the fiber has been started. | ||
+ | */ | ||
+ | public function isStarted(): | ||
+ | |||
+ | /** | ||
+ | * @return bool True if the fiber is currently suspended. | ||
+ | */ | ||
+ | public function isSuspended(): | ||
+ | |||
+ | /** | ||
+ | * @return bool True if the fiber is currently running. | ||
+ | */ | ||
+ | public function isRunning(): | ||
+ | |||
+ | /** | ||
+ | * @return bool True if the fiber has completed execution (either returning or | ||
+ | | ||
+ | */ | ||
+ | public function isTerminated(): | ||
+ | } | ||
+ | </ | ||
=== Unfinished Fibers === | === Unfinished Fibers === | ||
- | Fibers that are not finished(do not complete execution) are destroyed | + | |
+ | Fibers that are not finished (do not complete execution) are destroyed | ||
+ | |||
+ | === Fiber Stacks === | ||
+ | |||
+ | Each fiber is allocated a separate C stack and VM stack on the heap. The C stack is allocated using '' | ||
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
- | Declares '' | ||
- | ===== Proposed PHP Version(s) ===== | + | Declares '' |
- | PHP 8.1 | + | |
===== Future Scope ===== | ===== Future Scope ===== | ||
- | === async/await keywords === | ||
- | Using an internally defined '' | ||
- | <code php> | + | The current implementation does not provide |
- | $awaitable = async functionOrMethod(); | + | |
- | // async modifies the call to return | + | ===== Proposed PHP Version(s) ===== |
- | await $awaitable; // Await the function result at a later point. | + | |
- | </ | + | PHP 8.1 |
- | === defer keyword === | ||
- | Fibers may be used to implement a '' | ||
===== Proposed Voting Choices ===== | ===== Proposed Voting Choices ===== | ||
+ | |||
Merge implementation into core, 2/3 required. | Merge implementation into core, 2/3 required. | ||
===== Patches and Tests ===== | ===== Patches and Tests ===== | ||
- | Implementation at [[https:// | + | |
+ | Implementation | ||
+ | |||
+ | [[https:// | ||
+ | |||
+ | [[https:// | ||
+ | |||
+ | ===== Examples ===== | ||
+ | |||
+ | This first simple example creates a fiber that immediately suspends with the string ''" | ||
+ | |||
+ | <code php> | ||
+ | $fiber = new Fiber(function (): void { | ||
+ | $value = Fiber:: | ||
+ | echo "Value used to resume fiber: ", $value, " | ||
+ | }); | ||
+ | |||
+ | $value = $fiber-> | ||
+ | |||
+ | echo "Value from fiber suspending: ", $value, " | ||
+ | |||
+ | $fiber-> | ||
+ | </ | ||
+ | |||
+ | This example will output the following: | ||
+ | |||
+ | < | ||
+ | Value from fiber suspending: fiber | ||
+ | Value used to resume fiber: test | ||
+ | </ | ||
+ | |||
+ | ---- | ||
+ | |||
+ | 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> | ||
+ | class EventLoop | ||
+ | { | ||
+ | private string $nextId = ' | ||
+ | private array $deferCallbacks = []; | ||
+ | private array $read = []; | ||
+ | private array $streamCallbacks = []; | ||
+ | |||
+ | public function run(): void | ||
+ | { | ||
+ | while (!empty($this-> | ||
+ | $defers = $this-> | ||
+ | $this-> | ||
+ | foreach ($defers as $id => $defer) { | ||
+ | $defer(); | ||
+ | } | ||
+ | |||
+ | $this-> | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private function select(array $read): void | ||
+ | { | ||
+ | $timeout = empty($this-> | ||
+ | if (!stream_select($read, | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | foreach ($read as $id => $resource) { | ||
+ | $callback = $this-> | ||
+ | unset($this-> | ||
+ | $callback($resource); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public function defer(callable $callback): void | ||
+ | { | ||
+ | $id = $this-> | ||
+ | $this-> | ||
+ | } | ||
+ | |||
+ | public function read($resource, | ||
+ | { | ||
+ | $id = $this-> | ||
+ | $this-> | ||
+ | $this-> | ||
+ | } | ||
+ | } | ||
+ | |||
+ | [$read, $write] = stream_socket_pair( | ||
+ | stripos(PHP_OS, | ||
+ | STREAM_SOCK_STREAM, | ||
+ | STREAM_IPPROTO_IP | ||
+ | ); | ||
+ | |||
+ | // Set streams to non-blocking mode. | ||
+ | stream_set_blocking($read, | ||
+ | stream_set_blocking($write, | ||
+ | |||
+ | $loop = new EventLoop; | ||
+ | |||
+ | // Read data in a separate fiber after checking if the stream is readable. | ||
+ | $fiber = new Fiber(function () use ($loop, $read): void { | ||
+ | echo " | ||
+ | |||
+ | $fiber = Fiber:: | ||
+ | $loop-> | ||
+ | Fiber:: | ||
+ | |||
+ | $data = fread($read, | ||
+ | |||
+ | echo " | ||
+ | }); | ||
+ | |||
+ | // Start the fiber, which will suspend while waiting for a read event. | ||
+ | $fiber-> | ||
+ | |||
+ | // Defer writing data to an event loop callback. | ||
+ | $loop-> | ||
+ | |||
+ | // Run the event loop. | ||
+ | $loop-> | ||
+ | </ | ||
+ | |||
+ | This script will output the following: | ||
+ | |||
+ | < | ||
+ | Waiting for data... | ||
+ | Received data: Hello, world! | ||
+ | </ | ||
+ | |||
+ | If this example were written in a similar order without fibers, the script would be unable to read from a socket before writing to it, as the call to '' | ||
+ | |||
+ | Below is a chart illustrating execution flow between '' | ||
+ | |||
+ | {{ https:// | ||
+ | |||
+ | ---- | ||
+ | |||
+ | The next few examples use the async framework [[https:// | ||
+ | |||
+ | amphp v3 uses an [[https:// | ||
+ | |||
+ | The '' | ||
+ | |||
+ | <code php> | ||
+ | use function Amp\defer; | ||
+ | use function Amp\delay; | ||
+ | |||
+ | // defer() creates a new fiber and starts it when the | ||
+ | // current fiber is suspended or terminated. | ||
+ | defer(function (): void { | ||
+ | delay(1500); | ||
+ | var_dump(1); | ||
+ | }); | ||
+ | |||
+ | defer(function (): void { | ||
+ | delay(1000); | ||
+ | var_dump(2); | ||
+ | }); | ||
+ | |||
+ | defer(function (): void { | ||
+ | delay(2000); | ||
+ | var_dump(3); | ||
+ | }); | ||
+ | |||
+ | // Suspend the main context with delay(). | ||
+ | delay(500); | ||
+ | var_dump(4); | ||
+ | </ | ||
+ | |||
+ | ---- | ||
+ | |||
+ | The next example again uses amphp v3 to demonstrate how the event loop fiber continues executing while the main thread is " | ||
+ | |||
+ | <code php> | ||
+ | use function Amp\async; | ||
+ | use function Amp\await; | ||
+ | use function Amp\defer; | ||
+ | use function Amp\delay; | ||
+ | |||
+ | // Note that the function declares int as a return type, not Promise or Generator, | ||
+ | // but executes as a coroutine. | ||
+ | function asyncTask(int $id): int { | ||
+ | // Nothing useful is done here, but rather acts as a substitute for async I/O. | ||
+ | delay(1000); | ||
+ | return $id; | ||
+ | } | ||
+ | |||
+ | $running = true; | ||
+ | defer(function () use (& | ||
+ | // This loop is to show how this fiber is not blocked by other fibers. | ||
+ | while ($running) { | ||
+ | delay(100); | ||
+ | echo " | ||
+ | } | ||
+ | }); | ||
+ | |||
+ | // Invoking asyncTask() returns an int after 1 second, but is executed concurrently. | ||
+ | $result = asyncTask(1); | ||
+ | var_dump($result); | ||
+ | |||
+ | // Simultaneously runs two new fibers, await their resolution in the main fiber. | ||
+ | // await() suspends the fiber until the given promise (or array of promises here) are resolved. | ||
+ | $result = await([ | ||
+ | async(fn() => asyncTask(2)), | ||
+ | async(fn() => asyncTask(3)), | ||
+ | ]); | ||
+ | var_dump($result); | ||
+ | |||
+ | $result = asyncTask(4); | ||
+ | var_dump($result); | ||
+ | |||
+ | // array_map() takes 2 seconds to execute as the two calls are not concurrent, but this shows | ||
+ | // that fibers are supported by internal callbacks. | ||
+ | $result = array_map(fn(int $value) => asyncTask($value), | ||
+ | var_dump($result); | ||
+ | |||
+ | $running = false; // Stop the loop in the fiber created with defer() above. | ||
+ | </ | ||
+ | |||
+ | ---- | ||
+ | |||
+ | Since fibers can be paused during calls within the PHP VM, fibers can also be used to create asynchronous iterators and generators. The example below uses amphp v3 to suspend a fiber within a generator, awaiting resolution of a '' | ||
+ | |||
+ | <code php> | ||
+ | use Amp\Delayed; | ||
+ | use function Amp\await; | ||
+ | |||
+ | function generator(): | ||
+ | yield await(new Delayed(500, | ||
+ | yield await(new Delayed(1500, | ||
+ | yield await(new Delayed(1000, | ||
+ | yield await(new Delayed(2000, | ||
+ | yield 5; | ||
+ | yield 6; | ||
+ | yield 7; | ||
+ | yield await(new Delayed(2000, | ||
+ | yield 9; | ||
+ | yield await(new Delayed(1000, | ||
+ | } | ||
+ | |||
+ | // Iterate over the generator as normal, but the loop will | ||
+ | // be suspended and resumed as needed. | ||
+ | foreach (generator() as $value) { | ||
+ | printf(" | ||
+ | } | ||
+ | |||
+ | // Argument unpacking also can use a suspending generator. | ||
+ | var_dump(...generator()); | ||
+ | </ | ||
+ | |||
+ | ---- | ||
+ | |||
+ | The example below shows how [[https:// | ||
+ | |||
+ | <code php> | ||
+ | use React\EventLoop\LoopInterface; | ||
+ | use React\Promise\PromiseInterface; | ||
+ | |||
+ | function await(PromiseInterface $promise, LoopInterface $loop): mixed | ||
+ | { | ||
+ | $fiber = Fiber:: | ||
+ | if ($fiber === null) { | ||
+ | throw new Error(' | ||
+ | } | ||
+ | |||
+ | $promise-> | ||
+ | fn(mixed $value) => $loop-> | ||
+ | fn(Throwable $reason) => $loop-> | ||
+ | ); | ||
+ | |||
+ | return Fiber:: | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | A demonstration of integrating ReactPHP with fibers has been implemented in [[https:// | ||
+ | |||
+ | ===== FAQ ===== | ||
+ | |||
+ | === Who is the target audience for this feature? === | ||
+ | |||
+ | Fibers are an advanced feature that most users will not use directly. This feature is primarily targeted at library and framework authors to provide an event loop and an asynchronous programming API. Fibers allow integrating asynchronous code execution seamlessly into synchronous code at any point without the need to modify the application call stack or add boilerplate code. | ||
+ | |||
+ | **The Fiber API is not expected to be used directly in application-level code. Fibers provide a basic, low-level flow-control API to create higher-level abstractions that are then used in application code.** | ||
+ | |||
+ | '' | ||
+ | |||
+ | === What about performance? | ||
+ | |||
+ | Switching between fibers is lightweight, | ||
+ | |||
+ | === What platforms are supported? === | ||
+ | |||
+ | Fibers are supported on nearly all modern CPU architectures, | ||
+ | |||
+ | '' | ||
+ | |||
+ | === How are execution stacks swapped? === | ||
+ | |||
+ | Each fiber holds a pointer to a C stack and a VM stack ('' | ||
+ | |||
+ | Functions such as '' | ||
+ | |||
+ | === How does blocking code affect fibers === | ||
+ | |||
+ | Blocking code (such as '' | ||
+ | |||
+ | As fibers allow transparent use of asynchronous I/O, blocking implementations can be replaced by non-blocking implementations without affecting the entire call stack. If an internal event loop is available in the future, internal functions such as '' | ||
+ | |||
+ | === How do various fibers access the same memory? === | ||
+ | |||
+ | All fibers exist within a single thread. Only a single fiber may execute at a time, so memory cannot be accessed or modified simultaneously by multiple fibers, unlike threads which may modify memory simultaneously. | ||
+ | |||
+ | As fibers are suspended and resumed, execution of multiple fibers that access the same memory can be interleaved. Thus a running fiber may modify memory depended upon by another suspended fiber. There are various strategies to address this problem, including mutexes, semaphores, memory parcels, and channels. This RFC does not provide any such implementations as these can be implemented in user space code using the proposed fiber API. | ||
+ | |||
+ | === Why add this to PHP core? === | ||
+ | |||
+ | Adding this capability directly in PHP core makes it widely available on any host providing PHP. Often users are not able to determine what extensions may be available in a particular hosting environment, | ||
+ | |||
+ | Extensions that profile code need to account for switching fibers when creating backtraces and calculating execution times. This needs to be provided as a core internal API so any profiler could support fibers. The internal API that would be provided is out of scope of this RFC as it would not affect user code. | ||
+ | |||
+ | === Why not add an event loop and async/await API to core? === | ||
+ | |||
+ | This RFC proposes only the bare minimum required to allow user code to implement full-stack coroutines or green-threads in PHP. There are several frameworks that implement their own event loop API, promises, and other asynchronous APIs. These APIs vary greatly and are opinionated, | ||
+ | |||
+ | It is the opinion of the 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. | ||
+ | |||
+ | === How does this proposal differ from prior Fiber proposals? === | ||
+ | |||
+ | The prior [[https:// | ||
+ | |||
+ | === Are fibers compatible with extensions, including Xdebug? === | ||
+ | |||
+ | Fibers do not change how the PHP VM executes PHP code and suspending is supported within the C stack, so fibers are compatible with PHP extensions that simply provide a bridge to a C API, including those using callbacks that may call '' | ||
+ | |||
+ | Some extensions hook into the PHP VM and therefore are of particular interest for compatibility. | ||
+ | |||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | |||
+ | As noted in [[# | ||
+ | |||
+ | ===== Vote ===== | ||
+ | |||
+ | Voting started on 2021-03-08 and will run through 2021-03-22. 2/3 required to accept. | ||
+ | |||
+ | <doodle title=" | ||
+ | * Yes | ||
+ | * No | ||
+ | </ | ||
===== References ===== | ===== References ===== | ||
+ | |||
* [[https:// | * [[https:// | ||
* [[https:// | * [[https:// | ||
- | * [[https://wingolog.org/archives/2018/ | + | * [[https://www.lua.org/pil/9.1.html|Lua Fibers]] |
* [[https:// | * [[https:// | ||
rfc/fibers.txt · Last modified: 2021/07/12 21:30 by kelunik