From 1c85a0faa4b8ef466ba5965d974cb74f4e909e52 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Wed, 10 Aug 2022 16:47:57 -0700 Subject: [PATCH 1/7] Revised original event version Now uses blocks to signal event handlers Renamed to fibers --- proposals/tasks/Explainer.md | 596 +++++++++++++++++++++-------------- 1 file changed, 365 insertions(+), 231 deletions(-) diff --git a/proposals/tasks/Explainer.md b/proposals/tasks/Explainer.md index b323b2b3d..fc721bef9 100644 --- a/proposals/tasks/Explainer.md +++ b/proposals/tasks/Explainer.md @@ -1,138 +1,218 @@ -# Task Oriented Stack Switching +# Fiber Oriented Stack Switching ## Motivation Non-local control flow (sometimes called _stack switching_) operators allow applications to manage their own computations. Technically, this means that an application can partition its logic into separable computations and can manage (i.e., schedule) their execution. Pragmatically, this enables a range of control flows that include supporting the handling of asynchronous events, supporting cooperatively scheduled threads of execution, and supporting yield-style iterators and consumers. -This proposal refers solely to those features of the core WebAssembly virtual machine needed to support non-local control flow and does not attempt to preclude any particular strategy for implementing high level languages. We also aim for a minimal spanning set of concepts: the author of a high level language compiler should find all the necessary elements to enable their compiler to generate appropriate WebAssembly code fragments. +This proposal refers solely to those features of the core WebAssembly virtual machine needed to support non-local control flow and does not attempt to preclude any particular strategy for implementing high level languages. We also aim for a reasonably minimal spanning set of concepts[^0]: the author of a high level language compiler should find all the necessary elements to enable their compiler to generate appropriate WebAssembly code fragments. -## Tasks and Events +[^0]: _Minimality_ here means something slightly more than a strictly minimal set. In particular, if there is a feature that _could_ be implemented in terms of other features but that would incur significant penalties for many applications then we likely include it. -The main concepts in this proposal are the _task_ and the _event_. A task is a first class value which denotes a computation. An event is used to signal a change in the flow of computation from one task to another. +## Fibers and Events -### The `task` abstraction +The main concepts in this proposal are the _fiber_ and the _event_. A fiber is a first class value which denotes the resources used to support a computation. An event is used to signal a change in the flow of computation from one fiber to another. -A `task` denotes a computation that has an extent in time and may be referenced. The latter allows applications to manage their own computations and support patterns such as asynch/await, green threads and yield-style generators. +### The `fiber` concept -Associated with tasks are a new type: +A `fiber` is a resource that is used to enable a computation that has an extent in time and has identity. The latter allows applications to manage their own computations and support patterns such as async/await, green threads and yield-style generators. + +Associated with fibers is a new reference type: ``` -taskref +fiber r0 .. rk ``` -which denotes tasks. +which denotes fibers that return a vector of values of types `r0` through `rk`. + +#### The state of a fiber + +A fiber is inherently stateful; as the computation proceeds the fiber's state +reflects that evolution. The state of a fiber includes function call frames and +local variables that are currently live within the computation as well as +whether the fiber is currently being executed. In keeping with the main design +principles of WebAssembly, the frames and local variables represented within a +fiber are not accessible other than by normal operations. + +In addition to the frames and local variables, it is important to know whether a +fiber is suspendable or not. In essence, only fibers that have been explicitly +suspended may be resumed, and ony fibers that are either currently executing or +have resumed computations that are executing may be suspended. + +In general, validating that a suspended fiber may be resumed is a constant time +operation but validating that an executing fiber may be suspended involves +examining the chain of fiber resumptions. -Notice that our `taskref` type is not qualified: tasks are not expected to return in the normal way that functions do. We explore this topic again in [Frequently Asked Questions](#frequently-asked-questions). +Since a fiber may be referenced after it has completed, such references are not +usable and the referenced fibers are asserted to be moribund. +#### Fibers, ancestors and children -#### The state of a task +It is convenient to identify some relationships between fibers: the _resuming +parent_ of a fiber is the fiber that most recently resumed that fiber. A _resume +ancestor_ is either the _resume parent_ of the fiber, or is a resume ancestor of +the resume parent of the fiber. -A task is inherently stateful; as the computation proceeds the task's state reflects that evolution. The state of a task includes function call frames and local variables that are currently live within the computation as well as whether the task is currently being executed. +The root of the ancestor relation is the _root task_ and represents the initial +entry point into WebAssembly execution. -However, apart from when a significant event occurs within the computation, the state of a task is not externally visible. +The root fiber does not have an explicit identity. This is because, in general, +child fibers cannot manage their ancestors. -In our descriptions of task states it is convenient to identify an enumerated symbol that denotes the execution state of the task: +### The `event` concept + +An event is an occurrence where computation switches between fibers. Events also represent a communications opportunity between fibers: an event may communicate data as well as signal a change in fibers. + +#### Event declaration +Every change in computation is associated with an _event description_: whenever a fiber is suspended, the suspending fiber uses an event to signal both the reason for the suspension and to communicate any necessary data. Similarly, when a fiber is resumed, an event is used to signal to the resumed fiber the reason for the resumption. + +An event description has a predeclared `tag` which determines the type of event and what values are associated with the event. Event tags are declared: ``` -typedef enum { - suspended, - active, - moribund -} TaskExecutionState +(tag $e (param t*)) ``` +where the parameter types are the types of values communicated along with the event. Event tags may be exported from modules and imported into modules. -Only tasks that are in the `active` state may be suspended, and only tasks that are in the `suspended` state may be resumed. `moribund` tasks are those that are no longer capable of execution but may still be referenced in some way. The execution state of a task is not directly inspectable by WebAssembly instructions. +When computation is switched between fibers, the originating fiber must ensure that values corresponding to the event tag's signature are on the stack prior to the switch. After the switch, the values are available to the newly executing fiber, also on the value stack, and will no longer be available to the originating fiber. -#### Tasks, anscestors and children +For example, if an event description requires an `i32` parameter: -It is convenient to identify some relationships between tasks: the parent of a task is the task that most recently resumed that task. +``` +(tag $yield (param i32)) +``` +an `i32` value must be at the top of the value stack if the `$yield` event is signaled. If the switch event were a suspend (say), the suspending fiber must arrange to have the integer on the stack before issuing the `suspend` instruction. The resume parent of the suspending fiber will see this integer on the value stack—along with the `$yield` event itself. -The root of the ancestor relation is the _root task_ and represents the initial entry point into WebAssembly execution. +## Instructions -The root task does not have an explicit identity. This is because, in general, child tasks cannot manage their ancestors. +We introduce instructions for creating, suspending, resuming and terminating fibers and instructions for responding to events. -### The `event` abstraction +### Switching Blocks -An event is an occurrence where computation switches between tasks. Events also represent a communications opportunity between tasks: an event may communicate data as well as signal a change in tasks. +We manage the code that is responsible for handling switching events in terms of _handler blocks_. A handler block is a block that defines a localized context for handling switch events; in particular, within a handler block there may be zero or more _event_ blocks. -#### Event declaration -An event has a predeclared `tag` which determines the type of event and what values are associated with the event. Event tags are declared: +This block organization is reminiscent of how `try`..`catch` blocks are structured to enable exception handling; although there are significant differences. + +#### `handle` blocks + +A `handle` block introduces an execution scope where fibers may be suspended and/or resumed. Within a `handle` block, any event that arises as a result of a `suspend` or `resume` must be handled by a subsidiary `event` block. + +The overall text format of a `handle` block is: ``` -(tag $e (param t*)) +(handle $hdl_lbl + ... + fiber.suspend ... + ... + fiber.resume ... + ... + (event $evt_lbla $tag + ... + ) ;; $evt_lbla + (event $evt_lblb $tag + ... + ) ;; $evt_lblb +) ;; $hdl_lbl ``` -where the parameter types are the types of values communicated along with the event. Event tags may be exported from modules and imported into modules. -Every change in computation is associated with an event: whenever a task is suspended, the suspending task uses an event to signal both the reason for the suspension and to communicate any necessary data. Similarly, when a task is resumed, an event is used to signal to the resumed task the reason for the resumption. +although the ordering that is shown here is not required. -## Instructions +Any `fiber.suspend`, `fiber.resume`, `fiber.spawn` instruction must be textually enclosed within a `handle` block. This is because those instructions can all result in switches between fibers. However, a function body may be considered to be equivalent to an outermost `handle` block. + +Although it may be permitted to mix `suspend` and `resume` handling blocks, i.e., it is permitted to issue both `fiber.suspend` and `fiber.resume` instructions within a single `handle` block, in most cases they will not be mixed: a given `handle` block will be handling either a `fiber.suspend` or a `fiber.resume`, but not both. This is enforced, in part, by the assertion that instructions following a `fiber.suspend` instruction are not reachable. + +As with regular blocks, `handle` blocks also have a type signature. The `param` part of that signature indicates which values the block may expect to be on the value stack, and the `return` part of the signature indicated what values will be on the value stack at exit; _regardless_ of whether the `handle` block exits normally or via an `event` block. + +#### `event` blocks + +An `event` block specifies code that should be executed when a particular event occurs. An `event` block is associated with an event tag and a block signature. + +The event tag—which must have been declared in the `tag` section—signals the kind of event the block is intended to respond to. + +The block signature—which takes the normal form of a block signature—must match the signature associated with the tag. I.e., the event's signature must form a suffix of the block's parameter signature. Any additional elements of the block signature must form a suffix of the enclosing `handle` block. + +If an `event` block has a `return` signature, that repesents the exit signature for the event block. -We introduce instructions for managing tasks and instructions for signalling and responding to events. +`event` blocks are not executed if they are encountered normally: i.e., there is no fall-through into or between `event` blocks. In the case of an event block being encountered in the instruction flow, the immediately enclosing `handle` block is exited. -### Task instructions +When execution leaves an `event` block, the `handle` block in which it is located also exits. This is why the return signature of an `event` block must match the return signature of its enclosing `handle`block. -#### `task.new` Create a new task +As noted above, a function body is considered to be a `handle` block. This implies that there may be `event` blocks occurring within the top-level block of a function. In fact, we make use of this property for fibers that are created using the `fiber.new` instruction. -The `task.new` instruction creates a new task entity. The instruction has a literal operand which is the index of a function of type `[taskref t*]->[]`, together with corresponding values on the argument stack. +`handle` blocks may be nested; in which case, the available `event` blocks are the concatenation of all the `event` blocks in all the `handle` blocks that a given switching instruction is textually enclosed within—up to and including the function body itself. -The result is a `taskref` which is the identifier for the newly created task. The identity of the task is also the first argument to the task function itself—this allows tasks to know their own identity in a straightforward way. +### Fiber instructions -The task itself is created in a `suspended` state: it must be the case that the first executable instruction in the function body is an `event.switch` instruction. +#### `fiber.new` Create a new fiber -#### `task.suspend` Suspend an active task +The `fiber.new` instruction creates a new fiber. The instruction has a literal operand which is the index of a function of type `[[ref fiber r*] t*]->[r*]`, together with corresponding values on the argument stack. -The `task.suspend` instruction takes a task as an argument and suspends the task. The identified task must be in the `active` state—but it need not be the most recently activated task: it may be an ancestor of the most recent task. The _root_ ancestor task does not have an explicit identifier; and so it may not be suspended. +The result of the `fiber.new` instruction is a `fiber` which is the identifier for the newly created fiber. The identity of the fiber is also the first argument to the fiber function itself—this allows fibers to know their own identity in a straightforward way. -All the tasks between the most recently activated task and the identified task inclusive are marked as `suspended`. +The return values that the fiber function returns represent the return values of the fiber. This is reflected in the type signatures of the `fiber` and the function. -`task.suspend` has two operands: the identity of the task being suspended and a description of the event it is signaling: the `event` tag and any arguments to the event. The event operands must be on the argument stack. +The fiber itself is created in a `suspended` state: it must be the case that the function referenced has one or more `event` blocks within the top-level block of that function. When the fiber is resumed, one of those `event` blocks will be entered as a result of the `fiber.resume`—depending on the actual event used to resume the fiber. -The instruction following the `task.suspend` must be an `event.switch` instruction. +#### `fiber.spawn` Create and enter a new fiber -#### `task.resume` Resume a suspended task +The `fiber.spawn` instruction creates a new fiber and immediately enters it. It requires a literal function operand of type `[[fiber r*] t*] -> [r*]`, together with values corresponding to `t*` on the value stack. -The `task.resume` instruction takes a task as argument, together with an `event` description—consisting of an event tag and possible values, and resumes its execution. +The `fiber.spawn` instruction is analogous to a `call` instruction, the effect of the instruction is to leave on the value stack the return values of the fiber's function. However, as with other fibers, the new fiber may suspend; in which case there should be appropriate `event` blocks in the instruction stream enclosing the `fiber.spawn` instruction. -The `task.resume` instruction takes a `suspended` task, together with any descendant tasks that were suspended along with it, and resumes its execution. The event is used to encode how the resumed task should react: for example, whether the task's requested information is available, or whether the task should enter into cancelation mode. +Notice that only the newly created fiber has access to the reference that identifies the new fiber. If it is desired that the parent has access to this the code generator would typically insert a `fiber.suspend` instruction at the start of the function and pass out the `fiber` in an appropriate event description. -#### `task.switchto` Switch to a different task +#### `fiber.suspend` Suspend an active fiber -The `task.switchto` instruction is a combination of a `task.suspend` and a `task.resume` to an identified task. This instruction is useful for circumstances where the suspending task knows which other task should be resumed. +The `fiber.suspend` instruction takes a fiber as an argument and suspends the fiber. The identified fiber must either be the `active` fiber, or a resume ancestor of the active fiber. -The `task.switchto` instruction has three arguments: the identity of the task being suspended, the identity of the task being resumed and the signaling event. +The _root_ ancestor fiber does not have an explicit identifier; and so it may not be suspended. -Although it may be viewed as being a combination of the two instructions, there is an important distinction also: the signaling event. Under the common hierarchical organization, a suspending task does not know which task will be resumed. This means that the signaling event has to be of a form that the task's manager is ready to process. However, with a `task.switchto` instruction, the task's manager is not informed of the switch and does not need to understand the signaling event. +The fiber that is suspended is marked `suspended`, and the the immediate resume parent of that fiber becomes the active fiber. -This, in turn, means that a task manager may be relieved of the burden of communicating between tasks. I.e., `task.switchto` supports a symmetric coroutining pattern. However, precisely because the task's manager is not made aware of the switch between tasks, it must also be the case that this does not _matter_; in effect, the task manager may not directly be aware of any of the tasks that it is managing. +`fiber.suspend` has two operands: the identity of the fiber being suspended and a description of the event it is signaling: the `event` tag and any arguments to the event. The event operands must be on the argument stack. -#### `task.retire` Retire a task +Execution of a fiber that issues the `fiber.suspend` instruction will not continue until another fiber issues the appropriate `fiber.resume` instruction. In which case, execution of the suspending fiber will continue with the `event` block whose tag was used to resume it. In effect, this means that the instructions immediately following the `fiber.suspend` are _unreachable_. -The `task.retire` instruction is used when a task has finished its work and wishes to inform its parent of any final results. Like `task.suspend` (and `task.resume`), `task.retire` has an event argument—together with associated values on the agument stack— that are communicated. +#### `fiber.resume` Resume a suspended fiber -In addition, the retiring task is put into a `moribund` state and any computation resources associated with it are released. If the task has any active descendants then they too are made `moribund`. +The `fiber.resume` instruction takes a fiber as argument, together with an `event` description—consisting of an event tag and appropriate values, and resumes its execution. -#### `task.release` Destroy a suspended task +The `fiber.resume` instruction takes a `suspended` fiber, together with any descendant fibers that were suspended along with it, and resumes its execution. The event is used to encode how the resumed fiber should react: for example, whether the fiber's requested information is available, or whether the fiber should enter into cancelation mode. -The `task.release` instruction clears any computation resources associated with the identified task. The identified task must be in `suspended` state. +A `fiber.resume` instruction may continue in one of two ways: if the resumed fiber returned normally, then execution continues with the next instruction. The values on the value stack will match the return signature of the fiber's function—and will match the signature of the `fiber` reference that was resumed. -If the suspended task has current descendant tasks (such as when the task was suspended), then those tasks are `task.release`d also. +If the resumed fiber suspended itself, then the event tag associated with that `fiber.suspend` instruction is used to determine which of the available `event` blocks should be entered as part of the switch. The 'nearest' `event` block whose tag is equal to the supplied event is entered. If there is no appropriate `event` block in the execution scope of the fiber being resumed, then the engine _traps_. -Since task references are wasm values, the reference itself remains valid. However, the task itself is now in a `moribund` state that cannot be resumed. +#### `fiber.switch` Switch to a different fiber -The `task.release` instruction is primarily intended for situations where a task manage needs to eliminate unneeded task and does not wish to formally cancel them. +The `fiber.switch` instruction is a combination of a `fiber.suspend` and a `fiber.resume` to an identified fiber. This instruction is useful for circumstances where the suspending fiber knows which other fiber should be resumed. -### Event Instructions +The `fiber.switch` instruction has three arguments: the identity of the fiber being suspended, the identity of the fiber being resumed and the signaling event. -The main event instruction is `event.switch`; which is used to react to an event. +Although it may be viewed as being a combination of the two instructions, there is an important distinction also: the signaling event. Under the common hierarchical organization, a suspending fiber does not know which fiber will be resumed. This means that the signaling event has to be of a form that the fiber's manager is ready to process. However, with a `fiber.switch` instruction, the fiber's manager is not informed of the switch and does not need to understand the signaling event. -#### `event.switch` +This, in turn, means that a fiber manager may be relieved of the burden of communicating between fibers. I.e., `fiber.switch` supports a symmetric coroutining pattern. However, precisely because the fiber's manager is not made aware of the switch between fibers, it must also be the case that this does not _matter_; in effect, the fiber manager may not directly be aware of any of the fibers that it is managing. -The `event.switch` instruction takes a list of pairs as a literal operand. Each pair consists of the identity of an event tag and a block label. +#### `fiber.retire` Retire a fiber -If an event is signaled that is not in the list of event/label pairs then the engine traps: there is no fall back or stack search implied by this instruction. +The `fiber.retire` instruction is used when a fiber has finished its work and wishes to inform its parent of any final results. Like `fiber.suspend` (and `fiber.resume`), `fiber.retire` has an event argument—together with associated values on the agument stack— that are communicated. -If an event is signaled for which there is an event label in the list, then it must also be the case that the top n elements of the argument stack are present and are of the right type. This is validated by a combination of the event declaration and the type signatures of the identified blocks[^2]. If there is a mismatch in type expectations, then the module does not validate. +In addition, the retiring fiber is put into a moribund state and any computation resources associated with it are released. If the fiber has any active descendants then they too are made moribund. -[^2]: If there are more types in the block's result type, then those must correspond to elements of the input of that block. +#### `fiber.retireto` Retire a fiber and directly switch + +The `fiber.retireto` instruction is used when a fiber has finished its work and wishes to switch to another fiber. This is analogous to tail recursive calls of functions: the current fiber is retiring and another fiber is resumed. + +The `fiber.retireto` instruction has three operands: the identity of the fiber being retired, the identity of the fiber being resumed and an event —together with associated values on the agument stack— to communicate to the newly resumed fiber. + +In addition, the retiring fiber is put into a moribund state and any computation resources associated with it are released. + +#### `fiber.release` Destroy a suspended fiber + +The `fiber.release` instruction clears any computation resources associated with the identified fiber. The identified fiber must be in `suspended` state. + +If the suspended fiber has current descendant fibers (such as when the fiber was suspended), then those fibers are `fiber.release`d also. + +Since fiber references are wasm values, the reference itself remains valid. However, the fiber itself is now in a moribund state that cannot be resumed. + +The `fiber.release` instruction is primarily intended for situations where a fiber manager needs to eliminate unneeded fibers and does not wish to formally cancel them. ## Examples @@ -146,7 +226,7 @@ The so-called yield style generator pattern consists of a pair: a generator func We start with a simple C-style pseudo-code example of a generator that yields for every element of an array: ``` -void arrayGenerator(task *thisTask,int count,int els){ +void generator arrayGenerator(fiber *thisTask,int count,int els){ for(int ix=0;ixIn practice, the style of generators and consumers is dictated by the toolchain. It would have been possible to structure our generator differently. For example, if generators were created as suspended fibers—using `fiber.new` instructions—then the initial code of our generator would not have suspended using the `$identify` event. However, in that case, the top-level of the generator function should consist of an `event` block: corresponding to the first `$next` event the generator receives. +>In any case, generator functions enjor a somewhat special relationship with their callers and their structure reflects that. -Again, as with the generator, if an event is signaled to the consumer that does not match either event tag, the engine will trap. A toolchain wishing to implement a more robust execution can arrange to have an additional tag used for exceptions, for example. We will see this in how we handle access asynchronous I/O functions. +Again, as with the generator, if an event is signaled to the consumer that does not match either event tag, the engine will trap. ### Cooperative Coroutines -Cooperative coroutines, sometimes known as _green threads_ or _fibers_ allow an application to be structured in such a way that different responsibilities may be handled by different computations. The reasons for splitting into such fibers may vary; but one common scenario is to allow multiple sessions to proceed at their own pace. +Cooperative coroutines, sometimes known as _green threads_ or _fibers_ allow an application to be structured in such a way that different responsibilities may be handled by different computations. The reasons for splitting into such threads may vary; but one common scenario is to allow multiple sessions to proceed at their own pace. -In our formulation of fibers, we take an _arena_ based approach: when a program wishes to fork into separate fibers it does so by creating an arena or pool of fibers that represent the different activities. The arena computation as a whole only terminates when all of the fibers within it have completed. This allows a so-called _structured concurrency_ architecture that greatly enhances composability[^1]. +In our formulation of green threads, we take an _arena_ based approach: when a program wishes to fork into separate threads it does so by creating an arena or pool of fibers that represent the different activities. The arena computation as a whole only terminates when all of the threads within it have completed. This allows a so-called _structured concurrency_ architecture that greatly enhances composability[^1]. [^1]: However, how cooperative coroutines are actually structured depends on the source language and its approach to handling fibers. We present one alternative. -#### Structure of a Fiber -We start with a sketch of a fiber, in C-style pseudo-code, that adds a collection of generated numbers, but yielding to the arena scheduler between every number: +Our `$arrayGenerator` was structured so that it was entered using a `fiber.spawn` instruction; which implied that the `$arrayGenerator`'s first operation involved suspending itself with an `$identify` event. +In our threads example, we will take a different approach: each thread will be associated with a fiber function, but will be created as a suspended fiber. This allows the fiber arena manager to properly record the identity of each thread as it is created and to separately schedule the execution of its managed threads. + +#### Structure of a Green Thread +We start with a sketch of a thread, in C-style pseudo-code, that adds a collection of generated numbers, but yielding to the arena scheduler between every number: ``` -void adderFiber(task *thisTask, task *generatorTask){ +void fiber adderThread(fiber *thisThred, fiber *generatorTask){ int total = 0; while(true){ - switch(pause_fiber(thisTask)){ - case cancel_fiber: + switch(pause_thread(thisThred)){ + case cancel_thread: return; // Should really cancel the generator too case go_ahead_fiber:{ switch(next(generator)){ @@ -275,7 +382,7 @@ void adderFiber(task *thisTask, task *generatorTask){ continue; case end: // report the total somewhere - end_fiber(thisTask,total); + thread_thread(thisThred,total); return; } } @@ -283,64 +390,85 @@ void adderFiber(task *thisTask, task *generatorTask){ } } ``` -Note that we cannot simply use the previous function we constructed because we want to pause the fiber between every number. A more realistic scenario would not pause so frequently. +Note that we cannot simply use the previous consumer function we constructed because we want to pause the fiber between every number. A more realistic scenario would not pause so frequently. -The WebAssembly version of `adderFiber` is straightforward: +The WebAssembly version of `adderThread` is straightforward: ``` -(tag $pause_fiber) -(tag $end_fiber (param i32)) -(tag $go_ahead_fiber) -(tag $cancel_fiber) - -(func $adderFiber (param $thisTask taskref) (param $generator taskref) - (local $total i32) - (block $on-cancel - (block $on-init - (event.switch ($go-ahead $on-init)) - (unreachble) - ) +(tag $pause_thread) +(tag $thread_thread (param i32)) +(tag $go_ahead) +(tag $cancel_thread) +(type $thread ref fiber) + +(func $adderThread (param $thisThred $thread) (param $generator $generator) + (local $total i32 (i32.const 0)) + (event $cancel_thread) + (local.get $generator) + (fiber.release) ;; kill off the generator fiber + (local.get $total) ;; initially zero + (return) + ) + (event $go_ahead ;; event block at top-level of function (local.set $total (i32.const 0)) (loop $l - (block $on-end - (block $on-yield (i32) ;; 'returned' by the generator when it yields the next element - (task.resume (local.get $generator) ($next )) - (event.switch ($yield $on-yield) ($end $on-end)) + (handle $body + (handle $gen + (fiber.resume (local.get $generator) $next) + (br $body) ;; generator returned, so we're done + (event $yield (param i32) + (local.get $total) ;; next entry to add is already on the stack + (i32.add) + (local.set $total) + (fiber.suspend (local.get $thisThred) $pause_thread) + ) + (event $end + (br $body) + ) ) - (block $on-continue - (local.get $total) ;; next entry to add is already on the stack - (i32.add) - (local.set $total) - (task.yield (local.get $thisTask) ($pause_fiber)) - (event.switch ($go-ahead $on-continue) ($cancel_fiber $on-cancel)) - (unreachable) + (event $go_ahead + (br $l) ) - (br $l) ;; go back and do some more - ) - (task.retire (local.get $thisTask) ($end_fiber (local.get $total))) - (unreachable) - ) - ) ;; $on-cancel - (task.release (local.get $generator)) ;; Kill of the generator - (task.retire (local.get $thisTask) ($end_fiber (local.get $total))) - (unreachable)) + (event $cancel_thread + (br $body) ;; strictly redundant + ) + ) ;; $body + ) ;; $l + (fiber.retire (local.get $thisThred) ($thread_thread (local.get $total))) + ) +) ``` +The fiber function is structured to have an essentially empty top-level +body—it only consists of `event` handlers for the fiber protocol. This +protocol has two parts: when a fiber is resumed, it can be either told to +continue execution (using a `$go_ahead` event), or it can be told to cancel +itself (using a `$cancel_thread` event). + +This is at the top-level because fibers are created initially in suspended state. + +The main logic of the fiber function is a copy of the `$addAllElements` logic, rewritten to accomodate the fiber protocol. + A lot of the apparent complexity of this code is due to the fact that it has to embody two roles: it is a yielding client of the fiber manager and it is also the manager of a generator. Other than that, this is similar in form to `$addAllElements`. -#### Managing Fibers in an Arena -Managing multiple computations necessarily introduces the concept of _cancellation_: not all the computations launched will be needed. In our arena implementation, we launch several fibers and when the first one returns we will cancel the others: +>Notice that our `$adderFiber` function uses `fiber.release` to terminate the generator fiber. This avoids a potential memory leak in the case that the `$adderFiber` is canceled. +>In addition, the `$adderFiber` function does not return normally, it uses a `fiber.retire` instruction. This is used to slightly simplify our fiber arena manager. +#### Managing Fibers in an Arena +Managing multiple computations necessarily introduces the concept of +_cancellation_: not all the computations launched will be needed. In our arena +implementation, we launch several fibers and when the first one returns we will +cancel the others: ``` -int cancelingArena(task fibers[]){ +int cancelingArena(fiber fibers[]){ while(true){ // start them off in sequence for(int ix=0;ixWe don't include in this example, the code to construct the array of fibers. +>This is left as an exercise. The WebAssembly translation of this is complex but not involved: ``` @@ -358,148 +488,152 @@ The WebAssembly translation of this is complex but not involved: (loop $l (local.set $ix (i32.const 0)) (loop $for_ix - (table.get $task_table (i32.add (local.get $fibers)(local.get $ix))) - (task.resume ($go_ahead)) - (block $on-end (result i32) - (block $on-pause - (event.switch ($pause_fiber $on-pause)($end_fiber $on-end)) - (unreachable) - ) ;; pause_fiber event - (local.set $ix (i32.add (local.get $ix)(i32.const 1))) - (br_if $for_ix (i32.ge (local.get $ix) (local.get $len))) - ) ;; end_fiber event, found total on stack - (local.set $jx (i32.const 0)) - (loop $for_jx - (block $inner_jx - (br_if $inner_jx (i32.eq (local.get $ix)(local.get $jx))) - (table.get $task_table (i32.add (local.get $fibers)(local.get $jx))) - (task.resume ($cancel_fiber)) - (event.switch ($end_fiber $inner_jx)) ;; only acceptable event + (handle + (fiber.resume + (table.get $task_table (i32.add (local.get $fibers)(local.get $ix))) + $go_ahead) + (event $pause_thread + (local.set $ix (i32.add (local.get $ix)(i32.const 1))) + (br_if $for_ix (i32.ge (local.get $ix) (local.get $len))) + (br $l) + ) + (event thread_thread ;; We cancel all other fibers + (local.set $jx (i32.const 0)) + (loop $for_jx + (handle $inner_jx + (br_if $inner_jx (i32.eq (local.get $ix)(local.get $jx))) + (table.get $task_table (i32.add (local.get $fibers)(local.get $jx))) + (fiber.resume $cancel_thread) ;; cancel fibers != ix + (event $thread_thread ;; only acceptable event + (local.set $jx (i32.add (local.get $jx)(i32.const 1))) + (br_if $for_jx (i32.ge (local.get $jx)(local.get $len))) + (return) ;; total on stack + ) + (event $pause + (trap) + ) + ) + ) ) - (local.set $jx (i32.add (local.get $jx)(i32.const 1))) - (br_if $for_jx (i32.ge (local.get $jx)(local.get $len))) ) - (return) ;; total still on stack ) ) ) ``` -The main additional complications here don't come from tasks per se; but rather from the fact that we have to use a table to keep track of our fibers. +The main additional complications here don't come from threads per se; but rather from the fact that we have to use a table to keep track of our fibers. ### Asynchronous I/O TBD ## Frequently Asked Questions -### What is the difference between first class continuations and tasks? -A continuation is semantically a function that, when entered with a value, will finish the computation. In effect, continuations represent snapshots of the computation. A first class continuation is reified; i.e., it becomes a first class value and can be stored in tables and other locations. +### What is the difference between first class continuations and fibers? +A continuation is semantically a function that, when entered with a value, will finish the computation. In effect, continuations represent snapshots of computation. A first class continuation is reified; i.e., it becomes a first class value and can be stored in tables and other locations. -This is especially apparent when you compare delimited continuations and tasks. A task has a natural delimiter: the point in the overall computation where the task is created. Over the course of a task's computation, it may suspend and be resumed multiple times[^3]. Each point where a task is suspended, we may consider that a continuation exists that denotes the remainder of the task. +This is especially apparent when you compare delimited continuations and fibers. A fiber has a natural delimiter: the point in the overall computation where the fiber is created. Over the course of a fiber's computation, it may suspend and be resumed multiple times[^3]. Each point where a fiber is suspended, we may consider that a continuation exists that denotes the remainder of the fiber. -One salient aspect of first class continuations is _restartability_. In principal, a continuation can be restarted more than once. However, this proposal, as well as others under consideration, ban to resuability of computations. A design for computation management that depends on first class continuations must test for the attempted reuse of a continuation. +One salient aspect of first class continuations is _restartability_. In principal, a continuation can be restarted more than once. However, this proposal, as well as others under consideration, does not support the resuability of computations. Any design for computation management that depends on first class continuations must test for the attempted reuse of a continuation. However, this proposal does not reify continuations; instead the focus is on computations which do have an identity in this model. -[^3]: It can be reasonably argued that a computation that never suspends represents an anti-pattern. Setting up suspendable computations is associated with significant costs; and if it is known that a computation will not suspend then one should likely use a function instead of a task. +[^3]: It can be reasonably argued that a computation that never suspends represents an anti-pattern. Setting up suspendable computations is associated with significant costs; and if it is known that a computation will not suspend then one should likely use a function instead of a fiber. -### Can Continuations be modeled with tasks? -Within reason, this is straightforward. A task can be encapsulated into a function object in such a way that invoking the function becomes the equivalent of entering the continuation. +### Can Continuations be modeled with fibers? +Within reason, this is straightforward. A fiber can be encapsulated into a function object in such a way that invoking the function becomes the equivalent of entering the continuation. -However, this approach would not support restartable continuations without some additional ability to clone a task. This latter capability is not part of this proposal. +However, this approach would not support restartable continuations without some additional ability to clone a fiber. This latter capability is not part of this proposal. -### Can tasks be modeled with continuations? -Within reason, this too is straightforward. A task becomes an object that embeds a continuation. When the task is to be resumed, the embedded continuation is entered. +### Can fibers be modeled with continuations? +Within reason, this too is straightforward. A fiber becomes an object that embeds a continuation. When the fiber is to be resumed, the embedded continuation is entered. -Care would need to be taken in that the embedded continuation would need to be cleared; a more problematic issue is that, when a computation suspends, the correct task would have to be updated with the appropriate continuation. +Care would need to be taken in that the embedded continuation would need to be cleared; a more problematic issue is that, when a computation suspends, the correct fiber would have to be updated with the appropriate continuation. ### How are exceptions handled? -Exceptions arise in the context of suspendable computations because operations that are triggered prior to a suspension can fail. However, we do not make special accomodation for exceptions. Instead we use the common event mechanism to report both successful and unsuccessful computations. -When an I/O operation fails (say), and a requesting task needs to be resumed with that failure, then the resuming code (perhaps as part of an exception handler) resumes the suspended task with a suitable event. In general, all tasks, when they suspend, have to be prepared for three situations on their resumption: success, error and cancelation. This is best modeled in terms of an `event.switch` instruction listening for the three situations. +Fibers and fiber management have some conceptual overlap with exception handling. However, where exception handling is oriented to responding to exceptional situations and errors, fiber management is intended to model the normal—if non-local— flow of control. + +When an I/O operation fails (say), and a requesting fiber needs to be resumed with that failure, then the resuming code (perhaps as part of an exception handler) resumes the suspended fiber with a suitable event. In general, all fibers, when they suspend, have to be prepared for three situations on their resumption: success, error and cancelation. This is best modeled in terms of an `event.switch` instruction listening for the three situations. + +One popular feature of exception handling systems is that of _automatic exception propagation`; where an exception is automatically propagated from its point of origin to an outer scope that is equipped to respond to it. This proposal follows this by allowing unhandled exceptions to be propagated out of an executing fiber and into its resuming parent. -One popular feature of exception handling systems is that of _automatic exception propagation`; where an exception is automatically propagated from its point of origin to an outer scope that is equipped to respond to it. However, this policy is incompatible with any form of computation manipulation. +However, this policy is generally incompatible with any form of computation manipulation. -The reason is that, when a task is resumed, it may be from a context that does not at all resemble the original situation; indeed it may be resumed from a context that cannot handle any application exceptions. This happens today in the browser, for example. When a `Promise` is resumed, it is typically from the context of the so-called micro task runner. If the resumed code throws an exception the micro task runner would be excepted to deal with it. In practice, the micro task runner will silently drop all exceptions raised in this way. +The reason is that, when a fiber is resumed, it may be from a context that does not at all resemble the original situation; indeed it may be resumed from a context that cannot handle any application exceptions. This happens today in the browser, for example. When a `Promise` is resumed, it is typically from the context of the so-called micro fiber runner. If the resumed code throws an exception the micro fiber runner would be excepted to deal with it. In practice, the micro fiber runner will silently drop all exceptions raised in this way. -A more appropriate strategy for handling exceptions is for a specified sibling task, or at least a task that the language run-time is aware of, to handle the exception. This can be arranged by the language run-time straightforwardly by having the failing task signal an appropriate event. On the other hand, this kind of policy is extremely difficult to specify at the WebAssembly VM level. +A more appropriate strategy for handling exceptions is for a specified sibling fiber, or at least a fiber that the language runtime is aware of, to handle the exception. This can be arranged by the language runtime straightforwardly by having the failing fiber signal an appropriate event. -As a result, when a task throws an exception that is not caught by the task itself, we view this as a fatal error. There is no automatic propagation of exceptions out of tasks. +There is a common design element between this proposal and the exception handling proposal: the concept of an event. However, events as used in fiber oriented computation are explicitly intended to be as lightweight as possible. For example, there is no provision in events as described here to represent stack traces. Furthermore, events are not first class entities and cannot be manipulated, stored or transmitted. -### How do tasks fit in with structured concurrency? -The task-based approach works well with structured concurrency architectures. A relevant approach would likely take the form of so-called task _arenas_. A task arena is a collection of tasks under the management of some scheduler. All the tasks in the arena have the same manager; although a given task in an arena may itself be the manager of another arena. +### How do fibers fit in with structured concurrency? +The fiber-based approach works well with structured concurrency architectures. A relevant approach would likely take the form of so-called fiber _arenas_. A fiber arena is a collection of fibers under the management of some scheduler. All the fibers in the arena have the same manager; although a given fiber in an arena may itself be the manager of another arena. -This proposal does not enfore structured concurrency however. It would be quite possible, for example, for all of the tasks within a WebAssembly module to be managed by a single task scheduler. It is our opinion that this degree of choice is advisable in order to avoid unnecessary obstacles in the path of a language implementer. +This proposal does not enfore structured concurrency however. It would be quite possible, for example, for all of the fibers within a WebAssembly module to be managed by a single fiber scheduler. It is our opinion that this degree of choice is advisable in order to avoid unnecessary obstacles in the path of a language implementer. ### Are there any performance issues? Stack switching can be viewed as a technology that can be used to support suspendable computations and their management. Stack switching has been shown to be more efficient than approaches based on continuation passing style transformations[^4]. [^4]:Although CPS transformations do not require any change to the underlying engine; and they more readily can support restartable computations. -A task, as outlined here, can be viewed as a natural proxy for the stack in stack switching. I.e., a task entity would have an embedded link to the stacks used for that task. +A fiber, as outlined here, can be viewed as a natural proxy for the stack in stack switching. I.e., a fiber entity would have an embedded link to the stacks used for that fiber. -Furthermore, since the lifetime of a stack is approximately that of a task (a deep task may involve multiple stacks), the alignment of the two is good. In particular, a stack can be discarded precisely when the task is complete—although the task entity may still be referenced even though it is moribund. +Furthermore, since the lifetime of a stack is approximately that of a fiber (a deep fiber may involve multiple stacks), the alignment of the two is good. In particular, a stack can be discarded precisely when the fiber is complete—although the fiber entity may still be referenced even though it is moribund. On the other hand, any approach based on reifing continuations must deal with a more difficult alignment. The lifetime of a continuation is governed by the time a computation is suspended, not the whole lifetime. This potentially results in significant GC pressure to discard continuation objects after their utility is over. -### How do tasks relate to the JS Promise integration API? -A `Suspender` object, as documented in that API, corresponds reasonably well with a task. Like `Suspender`s, in order to suspend and resume tasks, there needs to be explicit communication between the top-level function of a task and the function that invokes suspension. +### How do fibers relate to the JS Promise integration API? +A `Suspender` object, as documented in that API, corresponds reasonably well with a fiber. Like `Suspender`s, in order to suspend and resume fibers, there needs to be explicit communication between the top-level function of a fiber and the function that invokes suspension. -A wrapped export in the JS Promise integration API can be realized using tasks quite straightforwardly: as code that creates a task and executes the wrapped export. Similarly, wrapping imports can be translated into code that looks for a `Promise` object and suspends the task as needed. - -### How does this proposal relate to exception handling? -Tasks and task management have some conceptua overlap with exception handling. However, where exception handling is oriented to responding to exceptional situations and errors, task management is intended to model the normal—if non-local— flow of control. - -There is a common design element between this proposal and the exception handling proposal: the concept of an event. However, events as used in task oriented computation are explicitly intended to be as lightweight as possible. For example, there is no provision in events as described here to represent stack traces. Furthermore, events are not first class entities and cannot be manipulated, stored or transmitted. +A wrapped export in the JS Promise integration API can be realized using fibers quite straightforwardly: as code that creates a fiber and executes the wrapped export. Similarly, wrapping imports can be translated into code that looks for a `Promise` object and suspends the fiber as needed. ### How does one support opt-out and opt-in? -The fundamental architecture of this proposal is capability based: having access to a task identifier allows a program to suspend and resume it. As such, opt-out is extremely straightforward: simply do not allow such code to become aware of the task identifier. +The fundamental architecture of this proposal is capability based: having access to a fiber identifier allows a program to suspend and resume it. As such, opt-out is extremely straightforward: simply do not allow such code to become aware of the fiber identifier. Supporting opt-in, where only specially prepared code can suspend and resume, and especially in the so-called _sandwich scenario_ is more difficult. If a suspending module invokes an import that reconnects to the module via another export, then this design will allow the module to suspend itself. This can invalidate execution assumptions of the sandwich filler module. It is our opinion that the main method for preventing the sandwich scenario is to prevent non-suspending modules from importing functions from suspending modules. Solutions to this would have greater utility than preventing abuse of suspendable computations; and perhaps should be subject to a different effort. -### Types and Tasks (or, why dont tasks have types?) - -Although we use functions to define the executable logic of tasks, such task functions naturally have a structure that is very different to normal functions. In particular, tasks communicate with each other via events which are not present in non-task functions. In addition, _task management_ requires a uniformity between tasks that is separate from the values computed by them. - -If one views a task as representing a computation with an extent in time, one can view them as having three phases: +### Why does a fiber function have to suspend immediately? -1. Initialization of local state, typically from the arguments to the task's initialization function; -1. communication with other tasks—using events; and -1. finalization of task, with a potential returning of a value. +The `fiber.new` instruction requires that the first executable instruction is an `event.switch` instruction. The primary reason for this is that, in many cases, eliminates an extraneous stack switch. -It seems likely that most of the communications involved with tasks is in the second phase; in addition, given the ability to communicate, the communication of task results may often be folded into the middle phase. +Fibers are created in the context of fiber management; of course, there are many flavors of fiber management depending on the application pattern being used. However, in many cases, the managing software must additionally perform other bookkeeping fibers (sic) when creating sub-computations. For example, in a green threading scenario, it may be necessary to record the newly created green thread in a scheduling data structure. -The type safety of individual events is guaranteed by the type signature of the event tag; when an event occurs the types of values communicated during the event is guaranteed by static type checking. The validity of the event itself must be checked dynamically: the recipient of the event must be prepared for the event in the corresponding `event.switch` instruction; otherwise the engine must trap. +By requiring the `fiber.new` instruction to not immediately start executing the new fiber we enable this bookkeeping to be accomplished with minimal overhead. -#### Tasks and session types +However, this proposal also includes the `fiber.spawn` instruction to accomodate those language runtimes that prefer the pattern of immediately executing new fibers. -A demerit of assigning a type to a task is that it would not be possible to fully capture the communication pattern of tasks; whereas, for functions, types are a better fit to capture how functions communicate (arguments and results). +### Isn't the 'search' for an event handler expensive? Does it involve actual search? +Although there is a list of `event` blocks that can respond to a switch event, +this does not mean that the engine has to conduct a linear search when decided +which code to execute. -Despite this, and the fact that individual communication events are statically typed—although some computation may be needed to verify which event is triggered— it is worth thinking about whether we can type an entire task computation. +Associated with each potential suspension point in a `handle` block one can construct a table of possible entry points; each table entry consisting of a program counter and an event tag. When a fiber is continued, this table is 'searched' for a corresponding entry that matches the actual event. -One approach that may support this would be to use [_session types_](http://www.dcs.gla.ac.uk/research/betty/summerschool2016.behavioural-types.eu/programme/DardhaIntroBST.pdf/at_download/file.pdf). Session types use algebraic data types (typically recursive) to model the state of a conversation between two or more parties. +Because both the table entries and any potential search key are all determined statically, the table search can be implemented using a [_perfect hash_](https://en.wikipedia.org/wiki/Perfect_hash_function) algorithm. I.e., a situation-specific hash algorithm that can guarantee constant access to the correct event handler. -We could model valid tasks by assigning them a session type and we would ensure _conversational integrity_ by requiring session types to match when creating tasks. +As a result, in practice, when switching between fibers and activating appropriate `event` blocks, there is no costly search involved. -While this may be a promising line of research, it seems that the gain from this (statically validating tasks vs dynamically validating each event) may not be sufficient to justify the effort. We are not currently planning on relying of session tyoes for this proposal. +### How does this concept of fiber relate to Wikipedia's concept +The Wikipedia definition of a [Fiber](https://en.wikipedia.org/wiki/Fiber_(computer_science)) is[^7]: -### Why does a task function have to suspend immediately? +>In computer science, a fiber is a particularly lightweight thread of execution. -The `task.new` instruction requires that the first executable instruction is an `event.switch` instruction. The primary reason for this is that, in many cases, eliminates an extraneous stack switch. +[^7]: As of 8/5/2022. -Tasks are created in the context of task management; of course, there are many flavors of task management depending on the application pattern being used. However, in many cases, the managing software must additionally perform other bookkeeping tasks (sic) when creating sub-computations. For example, in a green threading scenario, it may be necessary to record the newly created green thread in a scheduling data structure. +Our use of the term is consistent with that definition; but our principal modification is the concept of a `fiber`. In particular, this allows us to clarify that computations modeled in terms of fibers may be explicitly suspended, resumed etc., and that there may be a chain of fibers connected to each other via the resuming relationship. -By requiring the `task.new` instruction to not immediately start executing the new task we enable this bookkeeping to be accomplished with minimal overhead. +Our conceptualization of fibers is also intended to support [Structured Concurrency](https://en.wikipedia.org/wiki/Structured_concurrency). I.e., we expect our fibers to have a hierachical relationship and we also support high-level programming patterns involving groups of fibers and single exit of multiple cooperative activities. -### Why isn't the `event.switch` instruction folded in with `task.suspend` and `task.resume`? +## Open Design Issues +### The type signature of a fiber +The type signature of a fiber has a return type associated with it. This is not an essential requirement and may, in fact, cause problems in systems that must manage fibers. -Although it would be possible to combine the task switching instructions with the event response instructions there are a few reasons why this may not be optimal: +The reason for it is to accomodate the scenario of a fiber function returning. Since all functions must end at some point, establishing a reasonable semantics of what happens then seems important. -* The boundary between a `task.suspend` instruction and its following `event.switch` instruction represents a significant change in the semantic state of the WebAssembly machine. Merging instructions like this would require a concept such as _restarting the execution of an instruction in the middle_. The semantics of such half-executed instructions is problematic; and there is a reason that real CPUs tend not to have such instructions in their ISA. +Without the possiblity of returning normally, the only remaining recourse for a fiber would be to _retire_. We expect that, in many usage scenarios, this would be the correct way of ending the life of a fiber. -* The `event.switch` instruction is used in three situations: at the beginning of a task function, after a `task.suspend` and after a `task.resume` instruction. The first of these could not be eliminated without significant refactoring of the design. +### Exceptions are propagated +When an exception is thrown in the context of a fiber that is _not_ handled by the code of the fiber then that exception is propagated out of the fiber—into the code of the resuming parent. -* Not all task manipulation instructions are involved with an `event.switch` instruction. In particular, the `task.retire` instruction combines some of the semantics of a `task.suspend` with a `task.release`. +The biggest issue with this is that, for many if not most applications, the resuming parent of a parent is typically ill-equipped to handle the exception or to be able to recover gracefully from it. -* The potential code space saving is trivial: one opcode per `task.suspend`/`event.switch` combination. Any merged instruction would still need to support the literal argument vector associated with the `event.switch` instruction. From de94d2004d67cdfe614280c1d725680a4c3c7cbf Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 25 Aug 2022 16:14:37 -0700 Subject: [PATCH 2/7] Updated the explainer Added explanation of how to implement async/await Also did some word smithing --- proposals/tasks/Explainer.md | 294 +++++++++++++++++++++++++++-------- 1 file changed, 225 insertions(+), 69 deletions(-) diff --git a/proposals/tasks/Explainer.md b/proposals/tasks/Explainer.md index fc721bef9..a238a537f 100644 --- a/proposals/tasks/Explainer.md +++ b/proposals/tasks/Explainer.md @@ -4,13 +4,13 @@ Non-local control flow (sometimes called _stack switching_) operators allow applications to manage their own computations. Technically, this means that an application can partition its logic into separable computations and can manage (i.e., schedule) their execution. Pragmatically, this enables a range of control flows that include supporting the handling of asynchronous events, supporting cooperatively scheduled threads of execution, and supporting yield-style iterators and consumers. -This proposal refers solely to those features of the core WebAssembly virtual machine needed to support non-local control flow and does not attempt to preclude any particular strategy for implementing high level languages. We also aim for a reasonably minimal spanning set of concepts[^0]: the author of a high level language compiler should find all the necessary elements to enable their compiler to generate appropriate WebAssembly code fragments. +This proposal refers solely to those features of the core WebAssembly virtual machine needed to support non-local control flow and does not attempt to preclude any particular strategy for implementing high level languages. We also aim for a reasonably minimal spanning set of concepts[^a]: the author of a high level language compiler should find all the necessary elements to enable their compiler to generate appropriate WebAssembly code fragments. -[^0]: _Minimality_ here means something slightly more than a strictly minimal set. In particular, if there is a feature that _could_ be implemented in terms of other features but that would incur significant penalties for many applications then we likely include it. +[^a]: _Minimality_ here means something slightly more than a strictly minimal set. In particular, if there is a feature that _could_ be implemented in terms of other features but that would incur significant penalties for many applications then we likely include it. ## Fibers and Events -The main concepts in this proposal are the _fiber_ and the _event_. A fiber is a first class value which denotes the resources used to support a computation. An event is used to signal a change in the flow of computation from one fiber to another. +The main concepts in this proposal are the _fiber_ and the _event_. A fiber is a first class value which denotes the resource used to support a computation. An event is used to signal a change in the flow of computation from one fiber to another. ### The `fiber` concept @@ -34,15 +34,18 @@ fiber are not accessible other than by normal operations. In addition to the frames and local variables, it is important to know whether a fiber is suspendable or not. In essence, only fibers that have been explicitly -suspended may be resumed, and ony fibers that are either currently executing or +suspended may be resumed, and only fibers that are either currently executing or have resumed computations that are executing may be suspended. In general, validating that a suspended fiber may be resumed is a constant time operation but validating that an executing fiber may be suspended involves examining the chain of fiber resumptions. -Since a fiber may be referenced after it has completed, such references are not -usable and the referenced fibers are asserted to be moribund. +Even though a fiber may be referenced after it has completed, such references +are not usable and the referenced fibers are asserted to be _moribund_[^b]. Any +attempt to switch to a moribund fiber will result in a trap. + +[^b]: However, [an alternate approach](#using-tables-to-avoid-gc-pressure) suggests an alternate approach for managing moribund fibers. #### Fibers, ancestors and children @@ -59,19 +62,22 @@ child fibers cannot manage their ancestors. ### The `event` concept -An event is an occurrence where computation switches between fibers. Events also represent a communications opportunity between fibers: an event may communicate data as well as signal a change in fibers. +An event is an occurrence where execution switches between fibers. Events also +represent a communications opportunity between fibers: an event may communicate +data as well as signal a change in fibers. #### Event declaration Every change in computation is associated with an _event description_: whenever a fiber is suspended, the suspending fiber uses an event to signal both the reason for the suspension and to communicate any necessary data. Similarly, when a fiber is resumed, an event is used to signal to the resumed fiber the reason for the resumption. -An event description has a predeclared `tag` which determines the type of event and what values are associated with the event. Event tags are declared: +An event description has a predeclared `tag` which determines the type of event +and what values are associated with the event. Event tags are declared: ``` (tag $e (param t*)) ``` where the parameter types are the types of values communicated along with the event. Event tags may be exported from modules and imported into modules. -When computation is switched between fibers, the originating fiber must ensure that values corresponding to the event tag's signature are on the stack prior to the switch. After the switch, the values are available to the newly executing fiber, also on the value stack, and will no longer be available to the originating fiber. +When execution switches between fibers, the originating fiber must ensure that values corresponding to the event tag's signature are on the value stack prior to the switch. After the switch, the values are available to the newly executing fiber, also on the value stack, and will no longer be available to the originating fiber. For example, if an event description requires an `i32` parameter: @@ -80,13 +86,15 @@ For example, if an event description requires an `i32` parameter: ``` an `i32` value must be at the top of the value stack if the `$yield` event is signaled. If the switch event were a suspend (say), the suspending fiber must arrange to have the integer on the stack before issuing the `suspend` instruction. The resume parent of the suspending fiber will see this integer on the value stack—along with the `$yield` event itself. +Conversely, instructions that are _responding_ to an event can assume that the event tag's description are on the stack at the start of such a block. This is verified by the engine's validation algorithm which compares the block's signature with the event tag's signature. + ## Instructions We introduce instructions for creating, suspending, resuming and terminating fibers and instructions for responding to events. ### Switching Blocks -We manage the code that is responsible for handling switching events in terms of _handler blocks_. A handler block is a block that defines a localized context for handling switch events; in particular, within a handler block there may be zero or more _event_ blocks. +We manage the code that is responsible for handling switching events in terms of _handler blocks_. A handler block is a block that defines a localized context for handling switch events; in particular, within a handler block there may be zero or more _event_ blocks. Each event block contains code that is responsible for responding to a single event. This block organization is reminiscent of how `try`..`catch` blocks are structured to enable exception handling; although there are significant differences. @@ -94,21 +102,21 @@ This block organization is reminiscent of how `try`..`catch` blocks are structur A `handle` block introduces an execution scope where fibers may be suspended and/or resumed. Within a `handle` block, any event that arises as a result of a `suspend` or `resume` must be handled by a subsidiary `event` block. -The overall text format of a `handle` block is: +The overall format of a `handle` block is: ``` -(handle $hdl_lbl +(handle $hdl_lbl hdl_type ... fiber.suspend ... ... fiber.resume ... ... - (event $evt_lbla $tag + (event $evt_lbla $tag $type ... - ) ;; $evt_lbla - (event $evt_lblb $tag + ) + (event $evt_lblb $tag $type ... - ) ;; $evt_lblb + ) ) ;; $hdl_lbl ``` @@ -130,9 +138,9 @@ The block signature—which takes the normal form of a block signature&mdash If an `event` block has a `return` signature, that repesents the exit signature for the event block. -`event` blocks are not executed if they are encountered normally: i.e., there is no fall-through into or between `event` blocks. In the case of an event block being encountered in the instruction flow, the immediately enclosing `handle` block is exited. +`event` blocks are not executed if they are encountered normally: i.e., there is no fall-through into or between `event` blocks. In the case of an event block being encountered in the instruction stream, the immediately enclosing `handle` block is exited. -When execution leaves an `event` block, the `handle` block in which it is located also exits. This is why the return signature of an `event` block must match the return signature of its enclosing `handle`block. +When execution leaves an `event` block, the `handle` block in which it is located also exits. This is why the return signature of an `event` block must match the return signature of its enclosing `handle` block. As noted above, a function body is considered to be a `handle` block. This implies that there may be `event` blocks occurring within the top-level block of a function. In fact, we make use of this property for fibers that are created using the `fiber.new` instruction. @@ -148,7 +156,9 @@ The result of the `fiber.new` instruction is a `fiber` which is the identifier f The return values that the fiber function returns represent the return values of the fiber. This is reflected in the type signatures of the `fiber` and the function. -The fiber itself is created in a `suspended` state: it must be the case that the function referenced has one or more `event` blocks within the top-level block of that function. When the fiber is resumed, one of those `event` blocks will be entered as a result of the `fiber.resume`—depending on the actual event used to resume the fiber. +The new fiber is created in a suspended state: it must be the case that the function referenced has one or more `event` blocks within the top-level block of that function. When the fiber is resumed, one of those `event` blocks will be entered as a result of the `fiber.resume`—depending on the actual event used to resume the fiber. + +>If a function is referenced in a `fiber.new` instruction has no `event` blocks at the toplevel, then this function will cause a trap when attempting to resume the new fiber—because, without any `event` blocks, the fiber will not be able to respond to the resume. #### `fiber.spawn` Create and enter a new fiber @@ -196,11 +206,15 @@ The `fiber.retire` instruction is used when a fiber has finished its work and wi In addition, the retiring fiber is put into a moribund state and any computation resources associated with it are released. If the fiber has any active descendants then they too are made moribund. +>It is not recommended that a fiber allows exceptions to be propagated out of the fiber function. Instead, the function should use a `fiber.retire` —together with an appropriate event description—to signal the exceptional return. This allows the resume ancestor to directly capture the exceptional event as part of its normal response to the resume. + +>The reason that we don't recommend allowing exceptions to propagate is that an inapprpriate exception handler may be invoked as a result. This is especially dangerous in the case that the retiring fiber was switched to—with a `fiber.switch` instruction—rather than being resumed. + #### `fiber.retireto` Retire a fiber and directly switch The `fiber.retireto` instruction is used when a fiber has finished its work and wishes to switch to another fiber. This is analogous to tail recursive calls of functions: the current fiber is retiring and another fiber is resumed. -The `fiber.retireto` instruction has three operands: the identity of the fiber being retired, the identity of the fiber being resumed and an event —together with associated values on the agument stack— to communicate to the newly resumed fiber. +The `fiber.retireto` instruction has three operands: the identity of the fiber being retired, the identity of the fiber being resumed and an event —together with associated values on the agument stack—to communicate to the newly resumed fiber. In addition, the retiring fiber is put into a moribund state and any computation resources associated with it are released. @@ -212,7 +226,7 @@ If the suspended fiber has current descendant fibers (such as when the fiber was Since fiber references are wasm values, the reference itself remains valid. However, the fiber itself is now in a moribund state that cannot be resumed. -The `fiber.release` instruction is primarily intended for situations where a fiber manager needs to eliminate unneeded fibers and does not wish to formally cancel them. +The `fiber.release` instruction is primarily intended for situations where a fiber manager needs to eliminate unneeded fibers and does not wish to cancel them by resuming them with a cancellation event. ## Examples @@ -223,20 +237,21 @@ We look at three examples in order of increasing complexity and sophistication: The so-called yield style generator pattern consists of a pair: a generator function that generates elements and a consumer that consumes those elements. When the generator has found the next element it yields it to the consumer, and when the consumer needs the next element it waits for it. Yield-style generators represents the simplest use case for stack switching in general; which is why we lead with it here. #### Generating elements of an array -We start with a simple C-style pseudo-code example of a generator that yields for every element of an array: - +We start with a simple C-style pseudo-code example of a generator that yields for every element of an array. For explanatory purposes, we introduce a new `generator` function type and a new ``yield` statement to C: ``` void generator arrayGenerator(fiber *thisTask,int count,int els){ for(int ix=0;ixThe expression `generator resume next` is new syntax to resume a fiber with an +>identified event (in this case `next`); the value returned by the expression is +>the event signaled by the resumed fiber when it suspends. + +In WebAssembly, the `addAllElements` function takes the form: ``` (func $addAllElements (param $count i32) (param $els i32) (result i32) (local $generator $generator) @@ -356,33 +375,37 @@ Again, as with the generator, if an event is signaled to the consumer that does ### Cooperative Coroutines -Cooperative coroutines, sometimes known as _green threads_ or _fibers_ allow an application to be structured in such a way that different responsibilities may be handled by different computations. The reasons for splitting into such threads may vary; but one common scenario is to allow multiple sessions to proceed at their own pace. +Cooperative coroutines, sometimes known as _green threads_ allow an application to be structured in such a way that different responsibilities may be handled by different computations. The reasons for splitting into such threads may vary; but one common scenario is to allow multiple sessions to proceed at their own pace. -In our formulation of green threads, we take an _arena_ based approach: when a program wishes to fork into separate threads it does so by creating an arena or pool of fibers that represent the different activities. The arena computation as a whole only terminates when all of the threads within it have completed. This allows a so-called _structured concurrency_ architecture that greatly enhances composability[^1]. +In our formulation of green threads, we take an _arena_ based approach: when a program wishes to fork into separate threads it does so by creating an arena or pool of fibers that represent the different activities. The arena computation as a whole only terminates when all of the threads within it have completed. This allows a so-called _structured concurrency_ architecture that greatly enhances composability[^c]. -[^1]: However, how cooperative coroutines are actually structured depends on the source language and its approach to handling fibers. We present one alternative. +[^c]: However, how cooperative coroutines are actually structured depends on the source language and its approach to handling fibers. We present one alternative; many languages don't use structured concurrency techniques and collect all green threads into a single pool. Our `$arrayGenerator` was structured so that it was entered using a `fiber.spawn` instruction; which implied that the `$arrayGenerator`'s first operation involved suspending itself with an `$identify` event. -In our threads example, we will take a different approach: each thread will be associated with a fiber function, but will be created as a suspended fiber. This allows the fiber arena manager to properly record the identity of each thread as it is created and to separately schedule the execution of its managed threads. +In our threads example, we will take a different approach: each green thread +will be associated with a function, but will be created as a suspended fiber. +This allows the fiber arena manager to properly record the identity of each +thread as it is created and to separately schedule the execution of its managed +threads. #### Structure of a Green Thread -We start with a sketch of a thread, in C-style pseudo-code, that adds a collection of generated numbers, but yielding to the arena scheduler between every number: +We start with a sketch of a thread, in our C-style pseudo-code, that adds a collection of generated numbers, but yielding to the arena scheduler between every number: ``` void fiber adderThread(fiber *thisThred, fiber *generatorTask){ int total = 0; while(true){ - switch(pause_thread(thisThred)){ + switch(thisThred suspend pause_){ case cancel_thread: return; // Should really cancel the generator too case go_ahead_fiber:{ - switch(next(generator)){ + switch(generator resume next){ case yield(El): total += El; continue; case end: // report the total somewhere - thread_thread(thisThred,total); + thisThred suspend finish_(total); return; } } @@ -393,10 +416,9 @@ void fiber adderThread(fiber *thisThred, fiber *generatorTask){ Note that we cannot simply use the previous consumer function we constructed because we want to pause the fiber between every number. A more realistic scenario would not pause so frequently. The WebAssembly version of `adderThread` is straightforward: - ``` -(tag $pause_thread) -(tag $thread_thread (param i32)) +(tag $pause_) +(tag $yield_ (param i32)) (tag $go_ahead) (tag $cancel_thread) (type $thread ref fiber) @@ -420,7 +442,7 @@ The WebAssembly version of `adderThread` is straightforward: (local.get $total) ;; next entry to add is already on the stack (i32.add) (local.set $total) - (fiber.suspend (local.get $thisThred) $pause_thread) + (fiber.suspend (local.get $thisThred) $pause_) ) (event $end (br $body) @@ -434,7 +456,7 @@ The WebAssembly version of `adderThread` is straightforward: ) ) ;; $body ) ;; $l - (fiber.retire (local.get $thisThred) ($thread_thread (local.get $total))) + (fiber.retire (local.get $thisThred) ($finish_ (local.get $total))) ) ) ``` @@ -444,7 +466,7 @@ protocol has two parts: when a fiber is resumed, it can be either told to continue execution (using a `$go_ahead` event), or it can be told to cancel itself (using a `$cancel_thread` event). -This is at the top-level because fibers are created initially in suspended state. +This is at the top-level because our green thread fibers are created initially in suspended state. The main logic of the fiber function is a copy of the `$addAllElements` logic, rewritten to accomodate the fiber protocol. @@ -463,12 +485,12 @@ int cancelingArena(fiber fibers[]){ while(true){ // start them off in sequence for(int ix=0;ixWe don't include in this example, the code to construct the array of fibers. +>We don't include, in this example, the code to construct the array of fibers. >This is left as an exercise. The WebAssembly translation of this is complex but not involved: @@ -492,19 +514,19 @@ The WebAssembly translation of this is complex but not involved: (fiber.resume (table.get $task_table (i32.add (local.get $fibers)(local.get $ix))) $go_ahead) - (event $pause_thread + (event $pause_ (local.set $ix (i32.add (local.get $ix)(i32.const 1))) (br_if $for_ix (i32.ge (local.get $ix) (local.get $len))) (br $l) ) - (event thread_thread ;; We cancel all other fibers + (event finish_ ;; We cancel all other fibers (local.set $jx (i32.const 0)) (loop $for_jx (handle $inner_jx (br_if $inner_jx (i32.eq (local.get $ix)(local.get $jx))) (table.get $task_table (i32.add (local.get $fibers)(local.get $jx))) (fiber.resume $cancel_thread) ;; cancel fibers != ix - (event $thread_thread ;; only acceptable event + (event $finish_ ;; only acceptable event (local.set $jx (i32.add (local.get $jx)(i32.const 1))) (br_if $for_jx (i32.ge (local.get $jx)(local.get $len))) (return) ;; total on stack @@ -523,25 +545,139 @@ The WebAssembly translation of this is complex but not involved: The main additional complications here don't come from threads per se; but rather from the fact that we have to use a table to keep track of our fibers. ### Asynchronous I/O -TBD +In our third example, we look at integrating fibers with access to asynchronous APIs; which are accessed from module imports. + +On the web, asynchronous functions use the `Promise` pattern: an asynchronous I/O operation operates by first of all returning a `Promise` that 'holds' the I/O request, and at some point after the I/O operation is resolved a callback function attached to the `Promise` is invoked. + +>While non-Web embeddings of WebAssembly may not use `Promise`s in exactly the same way, the overall architecture of using promise-like entities to support async I/O is widespread. One specific feature that may be special to the Web is that it is not possible for an application to be informed of the result of an I/O request until after the currently executing code has completed and the browser's event loop has been invoked. + +#### Our Scenario +The JavaScript Promise Integration API (JSPI) allows a WebAssembly module to call a `Promise`-returning import and have it result in the WebAssembly module being suspended. In effect, using the JSPI results in the entire program being suspended. + +However, we would like to enable applications where several fibers can make independant requests to a `fetch` import and only 'return' when we have issued them all. Specifically, our example will involve multiple fibers making `fetch` requests and responding when the requests complete. + +This implies a combination of local scheduling of tasks, possibly a _tree_ of schedulers reflecting a hierarchical structure to the application, and, as we shall see, some form of `Promise` management. This latter aspect is perhaps unexpected but is forced on us by the common Web browser embedding: only the browser's outermost event loop is empowered to actually schedule tasks when I/O activities complete. + +#### A `fetch`ing Fiber +On the surface, our fibers that fetch data are very simple: +``` +async fetcher(string url){ + string text = await simple_fetch(url); + doSomething(text); +} +``` +In another extension to the C language, we have invented a new type of function—the `async` function. In our mythical extension, only `async` functions are permitted to use the `await` expression form. Our intention is that such a function has an implicit parameter: the `Fiber` that will be suspended when executing `await`. + +#### Importing `fetch` +The actual Web `fetch` is quite complex; and we do not intend to explore that complexity. Instead, we will use a simplified `simple_fetch` that takes a url string and returns a `Promise` of a `string`. (Again, we ignore issues such as failures of `fetch` here.) + +In order to properly connect the `Promise` returned by `simple_fetch` with our `fetcher` fiber, we need to associate the two entities. Since managing `Promise`s in WebAssembly is tedious, we will do this in JavaScript—via the `imported_fetch` function: + +``` +var currentFetches = []; +function imported_fetch(url){ + index = currentFetches.length; + P = fetch(url).then((Txt)=>{fiber:index,text:Txt}); + currentFetches.push(P); + return index; +} +``` +We also record the fiber/promise pair in our list of `currentFetches`. Actually, we use an index as a proxy for the fiber identity. In part, this is because Fiber objects are not JavaScript objects; the async-aware scheduler will maintain the mapping between this index and the fiber that created the `Promise`. + +The importance of this will become apparent below when we look at suspending schedulers. In addition to recording the `Promise` we also have to implement the suspension implied by the `await`[^d]. Since we cannot implement suspensions in JavaScript, we do this with 'glue' code: +``` +string simple_fetch(fiber *f,string url){ + int index = imported_fetch(url); + switch(f suspend async_io(index)){ + case io_result(Txt): + return Txt + } +} +``` +[^d]: On the Web, the convention is that creating a `Promise` _always_ results in a suspension; even if the item being waited for is already available. + +#### An async-aware scheduler +Implementing async functions requires that a scheduler is implemented within the language runtime library. Given the indirect nature of how `Promise`s are managed in our example, this leads to additional complexities for the scheduler. + +Our async-aware shceduler must, in addition to scheduling the green threads under its control, also arrange to suspend to the non-WebAssembly world of the browser in order to allow the I/O operations that were started to complete. + +The `currentFetches` list maintained by the JavaScript glue code has a mirror array that is used by the scheduler. This allows the scheduler to ensure that the correct fiber is resumed when the I/O operation completes. + +A simple, probably naïve, scheduler walks through a circular list of fibers to run through. Our one does that, but also records whenever a fiber is suspending due to a `Promise`d I/O operation: + +``` +typedef struct{ + Fiber f; + ResumeProtol reason; +} FiberState; +Queue fibers; +Map delayed; + +void naiveScheduler(){ + while(!fibers.isEmpty()){ + FiberState next = fibers.deQueue(); + switch(next.f resume next.reason){ + case pause: + fibers.push(FiberState{f:next,reason:go_ahead}); + break; + case async_io(Ix):{ + delayed.put(Ix,next); + break; + } + } + if(fibers.isEmpty() || pausing) + return; + } +} +``` +The vast majority of the code for this scheduler is _boilerplate_ code that is manipulating data structures. We leave it as an exercise for the reader to translate it into WebAssembly. + +#### Reporting a paused execution +The final piece of our scenario involves arranging for the WebAssembly application itself to pause so that browser's eventloop can complete the I/O operations and cause our code to be reentered. + +This is handled at the point where the WebAssembly is initially entered—i.e., through one of its exports. For the sake of exposition, we shall assume that we have a single export: `main`, which simply starts the scheduler: +``` +void main(){ + startScheduler(); +} +``` +The interesting logic is actually in the JavaScript glue code that will be invoked by the wider JavaScript application: +``` +var P = Promise.race(currentFetches).then((K)=> { + // We remove winning fetch from currentFetches ... + currentFetches[K.fiber] = undefined; + restart_scheduler(K.fiber,K.txt); +}); +``` +where `restart_scheduler` is a new auxilliary export that allows the top-level scheduler to resume a fiber with the appropriate text: +``` +void restart_scheduler(int fiberNo,string txt){ + Fiber next = delayed.get(fiberNo); + delayed.delete(fiberNo); + + fibers.push(FiberState{f:next,reason:reason:io_result(Txt)}); + start_scheduler(); +} +``` +We should not that this may not be the most efficient way of managing the collection of `Promise`s. In particular, if operations such as `Promise.race` involve linear scans of the `currentFetches` list, then we risk being quadratic in the number of outstanding fetches. However, this example is primarily intended to explain how one might implement the integration between WebAPIs such as `fetch` and a `Fiber` aware programming language. ## Frequently Asked Questions ### What is the difference between first class continuations and fibers? -A continuation is semantically a function that, when entered with a value, will finish the computation. In effect, continuations represent snapshots of computation. A first class continuation is reified; i.e., it becomes a first class value and can be stored in tables and other locations. +A continuation is semantically a function that, when entered with a value, will finish an identified computation. In effect, continuations represent snapshots of computations. A first class continuation is reified; i.e., it becomes a first class value and can be stored in tables and other locations. -This is especially apparent when you compare delimited continuations and fibers. A fiber has a natural delimiter: the point in the overall computation where the fiber is created. Over the course of a fiber's computation, it may suspend and be resumed multiple times[^3]. Each point where a fiber is suspended, we may consider that a continuation exists that denotes the remainder of the fiber. +The snapshot nature of a continuation is especially apparent when you compare delimited continuations and fibers. A fiber may give rise to multiple continuations—each time it suspends[^e] there is a new continuation implied by the state of the fiber. However, in this proposal, the fiber is reified wheras continuations are not. -One salient aspect of first class continuations is _restartability_. In principal, a continuation can be restarted more than once. However, this proposal, as well as others under consideration, does not support the resuability of computations. Any design for computation management that depends on first class continuations must test for the attempted reuse of a continuation. +One salient aspect of first class continuations is _restartability_. In principal, a reified continuation can be restarted more than once—simply by invoking it. -However, this proposal does not reify continuations; instead the focus is on computations which do have an identity in this model. +It would be possible to achieve the effect of restartability within a fibers design—by providing a means of _cloning_ fibers. -[^3]: It can be reasonably argued that a computation that never suspends represents an anti-pattern. Setting up suspendable computations is associated with significant costs; and if it is known that a computation will not suspend then one should likely use a function instead of a fiber. +However, this proposal, as well as others under consideration, does not support the restartability of continuations or cloning of fibers. -### Can Continuations be modeled with fibers? -Within reason, this is straightforward. A fiber can be encapsulated into a function object in such a way that invoking the function becomes the equivalent of entering the continuation. +[^e]: It can be reasonably argued that a computation that never suspends represents an anti-pattern. Setting up suspendable computations is associated with significant costs; and if it is known that a computation will not suspend then one should likely use a function instead of a fiber. -However, this approach would not support restartable continuations without some additional ability to clone a fiber. This latter capability is not part of this proposal. +### Can Continuations be modeled with fibers? +Within reason, this is straightforward. A fiber can be encapsulated into a function object in such a way that invoking the function becomes the equivalent of entering the continuation. This function closure would have to include a means of preventing the restartability of the continuation. ### Can fibers be modeled with continuations? Within reason, this too is straightforward. A fiber becomes an object that embeds a continuation. When the fiber is to be resumed, the embedded continuation is entered. @@ -570,9 +706,9 @@ The fiber-based approach works well with structured concurrency architectures. A This proposal does not enfore structured concurrency however. It would be quite possible, for example, for all of the fibers within a WebAssembly module to be managed by a single fiber scheduler. It is our opinion that this degree of choice is advisable in order to avoid unnecessary obstacles in the path of a language implementer. ### Are there any performance issues? -Stack switching can be viewed as a technology that can be used to support suspendable computations and their management. Stack switching has been shown to be more efficient than approaches based on continuation passing style transformations[^4]. +Stack switching can be viewed as a technology that can be used to support suspendable computations and their management. Stack switching has been shown to be more efficient than approaches based on continuation passing style transformations[^f]. -[^4]:Although CPS transformations do not require any change to the underlying engine; and they more readily can support restartable computations. +[^f]:Although CPS transformations do not require any change to the underlying engine; and they more readily can support restartable computations. A fiber, as outlined here, can be viewed as a natural proxy for the stack in stack switching. I.e., a fiber entity would have an embedded link to the stacks used for that fiber. @@ -585,10 +721,12 @@ A `Suspender` object, as documented in that API, corresponds reasonably well wit A wrapped export in the JS Promise integration API can be realized using fibers quite straightforwardly: as code that creates a fiber and executes the wrapped export. Similarly, wrapping imports can be translated into code that looks for a `Promise` object and suspends the fiber as needed. +However, as can be seen with the [asynchronous I/O example](#asynchronous-io), other complexities involving managing multiple `Promise`s have the combined effect of making the JSPI itself somewhat moot: for example, we had to multiplex multiple `Promise`s into a single one to ensure that, when an I/O `Promise` was resolved, our scheduler could be correctly woken up and it had to demultiplex the event into the correct sub-computation. + ### How does one support opt-out and opt-in? The fundamental architecture of this proposal is capability based: having access to a fiber identifier allows a program to suspend and resume it. As such, opt-out is extremely straightforward: simply do not allow such code to become aware of the fiber identifier. -Supporting opt-in, where only specially prepared code can suspend and resume, and especially in the so-called _sandwich scenario_ is more difficult. If a suspending module invokes an import that reconnects to the module via another export, then this design will allow the module to suspend itself. This can invalidate execution assumptions of the sandwich filler module. +Supporting full opt-in, where only specially prepared code can suspend and resume, and especially in the so-called _sandwich scenario_ is more difficult. If a suspending module invokes an import that reconnects to the module via another export, then this design will allow the module to suspend itself. This can invalidate execution assumptions of the sandwich filler module. It is our opinion that the main method for preventing the sandwich scenario is to prevent non-suspending modules from importing functions from suspending modules. Solutions to this would have greater utility than preventing abuse of suspendable computations; and perhaps should be subject to a different effort. @@ -614,11 +752,11 @@ Because both the table entries and any potential search key are all determined s As a result, in practice, when switching between fibers and activating appropriate `event` blocks, there is no costly search involved. ### How does this concept of fiber relate to Wikipedia's concept -The Wikipedia definition of a [Fiber](https://en.wikipedia.org/wiki/Fiber_(computer_science)) is[^7]: +The Wikipedia definition of a [Fiber](https://en.wikipedia.org/wiki/Fiber_(computer_science)) is[^g]: >In computer science, a fiber is a particularly lightweight thread of execution. -[^7]: As of 8/5/2022. +[^g]: As of 8/5/2022. Our use of the term is consistent with that definition; but our principal modification is the concept of a `fiber`. In particular, this allows us to clarify that computations modeled in terms of fibers may be explicitly suspended, resumed etc., and that there may be a chain of fibers connected to each other via the resuming relationship. @@ -632,8 +770,26 @@ The reason for it is to accomodate the scenario of a fiber function returning. S Without the possiblity of returning normally, the only remaining recourse for a fiber would be to _retire_. We expect that, in many usage scenarios, this would be the correct way of ending the life of a fiber. +### Fibers are started in suspended/running state +This proposal allows fibers to be created in a suspended state and immediately entered when `spawn`ed. + +Allowing fibers to be created in suspended state causes significant architectural issues in this design: in particular, because such a fiber has no prior history of execution (it *is* a new fiber), the fiber function has to be structured differently to account for the fact that there will be a resume event with no corresponding suspend event. + +On the other hand, requiring fibers to be started immediately on creation raises its own questions. In particular, if the spawner of a fiber also needs to record the identity of the fiber then the fiber must immediately suspend with some form of `identify` event. We saw this in the generator example. There are enough applications where this would result in a significant performance penalty, for example in a green threading library that is explicitly managing its fiber identities. + +For this reason, we support both forms of fiber creation. However, this also represents a compromise and added cost for implementation. + ### Exceptions are propagated When an exception is thrown in the context of a fiber that is _not_ handled by the code of the fiber then that exception is propagated out of the fiber—into the code of the resuming parent. The biggest issue with this is that, for many if not most applications, the resuming parent of a parent is typically ill-equipped to handle the exception or to be able to recover gracefully from it. +### Using tables to avoid GC pressure + +One of the consequences of having first class references to resources is that there is a potential for memory leaks. In this case, it may be that an application 'holds on' to a `fiber` references after that `fiber` has terminated. + +When a `fiber` terminates, the stack resources it uses may be released; however, the reference itself remains. Recall that, in linear memory WebAssembly, any application that wishes to keep a reference to a `fiber` has three choices: to store the reference in a global variable, to store it in a table or to keep it in a local variable. + +An alternative approach could be based on _reusing_ `fiber` references. In particular, if we allow a moribund fiber to be reused then the issue of garbage collecting old `fiber` references becomes a problem for the toolchain to address: it would become responsible for managing the `fiber` references it has access to. + +A further restriction would enhance this: if the only place where a `fiber` reference could be stored was in a table, then, if the default value for a `fiber` table entry were a moribund `fiber`, complete reponsibility for managing `fiber` references could be left to the toolchain. \ No newline at end of file From 85b28a62e05b4aa8b5b290563daee1b5f27f0c96 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 30 Aug 2022 16:09:32 -0700 Subject: [PATCH 3/7] Added text from Ross's rationale Edited down, and added as a FAQ --- proposals/tasks/Explainer.md | 41 ++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/proposals/tasks/Explainer.md b/proposals/tasks/Explainer.md index a238a537f..0144c3bb3 100644 --- a/proposals/tasks/Explainer.md +++ b/proposals/tasks/Explainer.md @@ -663,6 +663,30 @@ We should not that this may not be the most efficient way of managing the collec ## Frequently Asked Questions +### Why are we using 'lexical scoping' rather than 'dynamic scoping' +A key property of this design is that, in order for a WebAssembly program to switch fibers, the target of the switch is explicitly identified. This so-called lexical scoping approach is in contract with a dynamic approach—commonly used for exception handling—where the engine is expected to search the current evaluation context to decide where to suspend to (say). + +#### Implementation +In a lexically-scoped design, the engine is explicitly told by the program where to transfer control to. +Thus, the only additional obligation the engine has to implement, besides the actual transfer of control, is validation that the target is _valid_ from the current control point. + +In a dynamically-scoped design, the engine has to search for the transfer target. This search is typically not a simple search to specify and/or implement since the _criteria_ for a successful search depends on the language, both current and future. + +By requiring the program to determine the target, the computation of this target becomes a burden for the toolchain rather than for the WebAssembly engine implementor. + +#### Symmetric coroutining (and its cousin: task chaining) +With symmetric coroutines you can have a (often binary) collection of coroutines that directly yield to each other via application-specific messages. We saw a simple example of this in our [generator example](#generating-elements-of-an-array). + +Similar patterns arise when chaining tasks together, where one computation is intended to be followed by another. Involving a scheduler in this situation creates difficulties for types (the communication patterns between the tasks is often private and the types of data are not known to a general purpose scheduler). + +A lexically-scoped design more directly/simply/efficiently supports these common horizonal control-transfer patterns than a dynamically-scoped design. + +#### Composing Components +In applications where multiple _components_ are combined to form an application the risks of dynamic scoping may be unacceptable. By definition, components have a _shared nothing_ interface where the only permitted communications are those permitted by a common _interface language_. This includes prohibiting exceptions to cross component boundaries—unless via coercion—and switching between tasks. + +By requiring explicit fiber identifiers we make the task (sic) of implementing component boundaries more manageable when coroutining is involved. In fact, this is envisaged in the components design by using _streaming_ and _future_ metaphors to allow for this kind of control flow between components. + + ### What is the difference between first class continuations and fibers? A continuation is semantically a function that, when entered with a value, will finish an identified computation. In effect, continuations represent snapshots of computations. A first class continuation is reified; i.e., it becomes a first class value and can be stored in tables and other locations. @@ -763,6 +787,9 @@ Our use of the term is consistent with that definition; but our principal modifi Our conceptualization of fibers is also intended to support [Structured Concurrency](https://en.wikipedia.org/wiki/Structured_concurrency). I.e., we expect our fibers to have a hierachical relationship and we also support high-level programming patterns involving groups of fibers and single exit of multiple cooperative activities. ## Open Design Issues + +This section identifies areas of the design that are either not completely resolved or have significant potential for variability. + ### The type signature of a fiber The type signature of a fiber has a return type associated with it. This is not an essential requirement and may, in fact, cause problems in systems that must manage fibers. @@ -770,8 +797,15 @@ The reason for it is to accomodate the scenario of a fiber function returning. S Without the possiblity of returning normally, the only remaining recourse for a fiber would be to _retire_. We expect that, in many usage scenarios, this would be the correct way of ending the life of a fiber. +### Exceptions are propagated +When an exception is thrown in the context of a fiber that is _not_ handled by the code of the fiber then that exception is propagated out of the fiber—into the code of the resuming parent. + +The biggest issue with this is that, for many if not most applications, the resuming parent of a parent is typically ill-equipped to handle the exception or to be able to recover gracefully from it. + +The issue is exacerbated by the fact that functions are _not_ annotated with any indication that they may throw an exception let alone what type of exception they may throw. + ### Fibers are started in suspended/running state -This proposal allows fibers to be created in a suspended state and immediately entered when `spawn`ed. +This proposal allows fibers to be created in a suspended state or they can be created and immediately entered when `spawn`ed. Allowing fibers to be created in suspended state causes significant architectural issues in this design: in particular, because such a fiber has no prior history of execution (it *is* a new fiber), the fiber function has to be structured differently to account for the fact that there will be a resume event with no corresponding suspend event. @@ -779,11 +813,6 @@ On the other hand, requiring fibers to be started immediately on creation raises For this reason, we support both forms of fiber creation. However, this also represents a compromise and added cost for implementation. -### Exceptions are propagated -When an exception is thrown in the context of a fiber that is _not_ handled by the code of the fiber then that exception is propagated out of the fiber—into the code of the resuming parent. - -The biggest issue with this is that, for many if not most applications, the resuming parent of a parent is typically ill-equipped to handle the exception or to be able to recover gracefully from it. - ### Using tables to avoid GC pressure One of the consequences of having first class references to resources is that there is a potential for memory leaks. In this case, it may be that an application 'holds on' to a `fiber` references after that `fiber` has terminated. From 3ec7fd6f2084a217df9f47a392f513c273b460b5 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Fri, 2 Sep 2022 14:36:00 -0700 Subject: [PATCH 4/7] Edited FAQ entry on relationship with JSPI to show actual potential code. --- proposals/tasks/Explainer.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/proposals/tasks/Explainer.md b/proposals/tasks/Explainer.md index 0144c3bb3..6425e01af 100644 --- a/proposals/tasks/Explainer.md +++ b/proposals/tasks/Explainer.md @@ -743,8 +743,38 @@ On the other hand, any approach based on reifing continuations must deal with a ### How do fibers relate to the JS Promise integration API? A `Suspender` object, as documented in that API, corresponds reasonably well with a fiber. Like `Suspender`s, in order to suspend and resume fibers, there needs to be explicit communication between the top-level function of a fiber and the function that invokes suspension. -A wrapped export in the JS Promise integration API can be realized using fibers quite straightforwardly: as code that creates a fiber and executes the wrapped export. Similarly, wrapping imports can be translated into code that looks for a `Promise` object and suspends the fiber as needed. +A wrapped export in the JS Promise integration API can be realized using fibers +quite straightforwardly: as code that creates a fiber and executes the wrapped +export. This can be seen in the pseudo-JavaScript fragment for the export +wrapper[^g]: +``` +function makeAsyncExportWrapper(wasmFn) { + return function(...args) { + return new Promise((resolve,reject) => { + spawn Fiber((F) => { + try{ + resolve(wasmFn(F,args)); + } catch (E) { + reject(E); + } + }) + }) + } + } +``` +[^g]: This code does not attempt to depict any _real_ JavaScript; if for no other reason than that we do not anticipate extending JavaScript with fibers. +Similarly, wrapping imports can be translated into code that attaches a callback to the incoming `Promise` that will resume the fiber with the results of the `Promise`: +``` +function makeAsyncImportWrapper(jsFn) { + return function(F,...args) { + jsFn(...args).then(result => { + F.resume(result); + }); + F.suspend() + } +} +``` However, as can be seen with the [asynchronous I/O example](#asynchronous-io), other complexities involving managing multiple `Promise`s have the combined effect of making the JSPI itself somewhat moot: for example, we had to multiplex multiple `Promise`s into a single one to ensure that, when an I/O `Promise` was resolved, our scheduler could be correctly woken up and it had to demultiplex the event into the correct sub-computation. ### How does one support opt-out and opt-in? @@ -776,11 +806,11 @@ Because both the table entries and any potential search key are all determined s As a result, in practice, when switching between fibers and activating appropriate `event` blocks, there is no costly search involved. ### How does this concept of fiber relate to Wikipedia's concept -The Wikipedia definition of a [Fiber](https://en.wikipedia.org/wiki/Fiber_(computer_science)) is[^g]: +The Wikipedia definition of a [Fiber](https://en.wikipedia.org/wiki/Fiber_(computer_science)) is[^h]: >In computer science, a fiber is a particularly lightweight thread of execution. -[^g]: As of 8/5/2022. +[^h]: As of 8/5/2022. Our use of the term is consistent with that definition; but our principal modification is the concept of a `fiber`. In particular, this allows us to clarify that computations modeled in terms of fibers may be explicitly suspended, resumed etc., and that there may be a chain of fibers connected to each other via the resuming relationship. From c37073b8c508e920f6722dc68c54576d7d73bbe7 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Fri, 9 Sep 2022 11:58:56 -0700 Subject: [PATCH 5/7] Some minor typos tweaks --- proposals/tasks/Explainer.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/proposals/tasks/Explainer.md b/proposals/tasks/Explainer.md index 6425e01af..8246bf6cb 100644 --- a/proposals/tasks/Explainer.md +++ b/proposals/tasks/Explainer.md @@ -569,7 +569,7 @@ async fetcher(string url){ In another extension to the C language, we have invented a new type of function—the `async` function. In our mythical extension, only `async` functions are permitted to use the `await` expression form. Our intention is that such a function has an implicit parameter: the `Fiber` that will be suspended when executing `await`. #### Importing `fetch` -The actual Web `fetch` is quite complex; and we do not intend to explore that complexity. Instead, we will use a simplified `simple_fetch` that takes a url string and returns a `Promise` of a `string`. (Again, we ignore issues such as failures of `fetch` here.) +The actual `fetch` Web API is quite complex; and we do not intend to explore that complexity. Instead, we will use a simplified `simple_fetch` that takes a url string and returns a `Promise` of a `string`. (Again, we ignore issues such as failures of `fetch` here.) In order to properly connect the `Promise` returned by `simple_fetch` with our `fetcher` fiber, we need to associate the two entities. Since managing `Promise`s in WebAssembly is tedious, we will do this in JavaScript—via the `imported_fetch` function: @@ -599,7 +599,7 @@ string simple_fetch(fiber *f,string url){ #### An async-aware scheduler Implementing async functions requires that a scheduler is implemented within the language runtime library. Given the indirect nature of how `Promise`s are managed in our example, this leads to additional complexities for the scheduler. -Our async-aware shceduler must, in addition to scheduling the green threads under its control, also arrange to suspend to the non-WebAssembly world of the browser in order to allow the I/O operations that were started to complete. +Our async-aware scheduler must, in addition to scheduling any green threads under its control, also arrange to suspend to the non-WebAssembly world of the browser in order to allow the I/O operations that were started to complete. The `currentFetches` list maintained by the JavaScript glue code has a mirror array that is used by the scheduler. This allows the scheduler to ensure that the correct fiber is resumed when the I/O operation completes. @@ -686,7 +686,6 @@ In applications where multiple _components_ are combined to form an application By requiring explicit fiber identifiers we make the task (sic) of implementing component boundaries more manageable when coroutining is involved. In fact, this is envisaged in the components design by using _streaming_ and _future_ metaphors to allow for this kind of control flow between components. - ### What is the difference between first class continuations and fibers? A continuation is semantically a function that, when entered with a value, will finish an identified computation. In effect, continuations represent snapshots of computations. A first class continuation is reified; i.e., it becomes a first class value and can be stored in tables and other locations. @@ -760,7 +759,7 @@ function makeAsyncExportWrapper(wasmFn) { }) }) } - } +} ``` [^g]: This code does not attempt to depict any _real_ JavaScript; if for no other reason than that we do not anticipate extending JavaScript with fibers. From 31757d1b6ca6defb01f22971e1147d9ad8650cbe Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Fri, 9 Sep 2022 16:07:25 -0700 Subject: [PATCH 6/7] Refactored the asynchronous I/O example Does not need Promise.race any more --- proposals/tasks/Explainer.md | 127 ++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/proposals/tasks/Explainer.md b/proposals/tasks/Explainer.md index 8246bf6cb..84768c7e8 100644 --- a/proposals/tasks/Explainer.md +++ b/proposals/tasks/Explainer.md @@ -556,7 +556,7 @@ The JavaScript Promise Integration API (JSPI) allows a WebAssembly module to cal However, we would like to enable applications where several fibers can make independant requests to a `fetch` import and only 'return' when we have issued them all. Specifically, our example will involve multiple fibers making `fetch` requests and responding when the requests complete. -This implies a combination of local scheduling of tasks, possibly a _tree_ of schedulers reflecting a hierarchical structure to the application, and, as we shall see, some form of `Promise` management. This latter aspect is perhaps unexpected but is forced on us by the common Web browser embedding: only the browser's outermost event loop is empowered to actually schedule tasks when I/O activities complete. +This implies a combination of local scheduling of tasks, possibly a _tree_ of schedulers reflecting a hierarchical structure to the application, and, as we shall see, some form of multiplexing of requests and demultiplexing of responses. This aspect is perhaps unexpected but is forced on us by the common Web browser embedding: only the browser's outermost event loop is empowered to actually schedule tasks when I/O activities complete. #### A `fetch`ing Fiber On the surface, our fibers that fetch data are very simple: @@ -571,37 +571,45 @@ In another extension to the C language, we have invented a new type of function& #### Importing `fetch` The actual `fetch` Web API is quite complex; and we do not intend to explore that complexity. Instead, we will use a simplified `simple_fetch` that takes a url string and returns a `Promise` of a `string`. (Again, we ignore issues such as failures of `fetch` here.) -In order to properly connect the `Promise` returned by `simple_fetch` with our `fetcher` fiber, we need to associate the two entities. Since managing `Promise`s in WebAssembly is tedious, we will do this in JavaScript—via the `imported_fetch` function: +Since it is our intention to continue execution of our application even while we are waiting for the `fetch`, we have to somewhat careful in how we integrate with the browser's event loop. In particular, we need to be able to separate which fiber is being _suspended_—when we encounter the `fetch`es `Promise`—and which fiber is resumed when the fetch data becomes _available_. +We can express this with the pseudo code: ``` -var currentFetches = []; -function imported_fetch(url){ - index = currentFetches.length; - P = fetch(url).then((Txt)=>{fiber:index,text:Txt}); - currentFetches.push(P); - return index; +function simple_fetch(client,url){ + fetch(url).then(response => { + scheduler resume io_notify(client,response.data); + }); + switch(client.suspend async_request){ + case io_result(text): return text; + } + } } ``` -We also record the fiber/promise pair in our list of `currentFetches`. Actually, we use an index as a proxy for the fiber identity. In part, this is because Fiber objects are not JavaScript objects; the async-aware scheduler will maintain the mapping between this index and the fiber that created the `Promise`. +Notice how the `simple_fetch` function invokes `fetch` and attaches a callback to it that resumes the `scheduler` fiber, passing it the identity of the actual `client` fiber and the results of the `fetch`. Before returning, `simple_fetch` suspends the `client` fiber; and a continuation of _that_ suspension will result in `text` being delivered to the client code. + +It is, of course, going to be the responsibility of the `scheduler` to ensure that the data is routed to the correct client fiber. + +#### A note about the browser event loop +It is worth digging in a little deeper why we have this extra level of indirection. Fundamentally, this arises due to a limitation[^d] of the Web browser architecture itself. The browser's event loop has many responsibilities; inluding the one of monitoring for the completion of asynchronous I/O activities initialized by the Web application. In addition, the _only_ way that an application can be informed of the completion (and success/failure) of an asynchronous operation is for the event loop to invoke the callback on a `Promise`. + +This creates a situation where our asynchronous WebAssembly application must ultimately return to the browser before any `fetch`es it has initiated can be delivered. However, this kind of return has to be through our application's own scheduler. And it must also be the case that any resumption of the WebAssembly application is initiated through the same scheduler. + +In particular, if the browser's event loop tries to directly resume the fiber that created the `Promise` we would end up in a situation that is very analogous to a _deadlock_: when that fiber is further suspended, or even if if completes, the application as a whole will stop—because other parts of the application are still waiting for the scheduler to be resumed; but that scheduler was waiting to be resumed by the browser's event loop scheduler. -The importance of this will become apparent below when we look at suspending schedulers. In addition to recording the `Promise` we also have to implement the suspension implied by the `await`[^d]. Since we cannot implement suspensions in JavaScript, we do this with 'glue' code: +[^d]: A better phrasing of this might be an unexpected consequence of the browser's event model. This limitation does not apply, for example, to normal desktop applications running in regular operating systems. + +The net effect of this is that, for browser-based applicatios, we must ensure that we _multiplex_ all I/O requests through the scheduler and _demultiplex_ the results of those requests back to the appropriate leaf fibers. The demultiplex is the reason why the actual callback call looks like: ``` -string simple_fetch(fiber *f,string url){ - int index = imported_fetch(url); - switch(f suspend async_io(index)){ - case io_result(Txt): - return Txt - } -} +scheduler resume io_notify(client,response.data) ``` -[^d]: On the Web, the convention is that creating a `Promise` _always_ results in a suspension; even if the item being waited for is already available. +This `resume` tells the scheduler to resume `client` with the data `response.data`. I.e., it is a kind of indirect resume: we resume the scheduler with an event that asks it to resume a client fiber. One additional complication of this architecture is that the scheduler must be aware of the types of data that Web APIs return. -#### An async-aware scheduler -Implementing async functions requires that a scheduler is implemented within the language runtime library. Given the indirect nature of how `Promise`s are managed in our example, this leads to additional complexities for the scheduler. +It is expected that, in a non-browser setting, one would not need to distort the architecture so much. In that case, the same scheduler that decides which fiber to resume next could also keep track of I/O requests as they become available. -Our async-aware scheduler must, in addition to scheduling any green threads under its control, also arrange to suspend to the non-WebAssembly world of the browser in order to allow the I/O operations that were started to complete. +#### An async-aware scheduler +Implementing async functions requires that a scheduler is implemented within the language runtime library. This is actually a consequence of having a special syntax for `async` functions. -The `currentFetches` list maintained by the JavaScript glue code has a mirror array that is used by the scheduler. This allows the scheduler to ensure that the correct fiber is resumed when the I/O operation completes. +Our async-aware scheduler must, in addition to scheduling any green threads under its control, also arrange to suspend to the non-WebAssembly world of the browser in order to allow the I/O operations that were started to complete. And we must also route the responses to the appropriate leaf fiber. A simple, probably naïve, scheduler walks through a circular list of fibers to run through. Our one does that, but also records whenever a fiber is suspending due to a `Promise`d I/O operation: @@ -611,55 +619,68 @@ typedef struct{ ResumeProtol reason; } FiberState; Queue fibers; -Map delayed; +List delayed; void naiveScheduler(){ - while(!fibers.isEmpty()){ - FiberState next = fibers.deQueue(); - switch(next.f resume next.reason){ - case pause: - fibers.push(FiberState{f:next,reason:go_ahead}); - break; - case async_io(Ix):{ - delayed.put(Ix,next); - break; + while(true){ + while(!fibers.isEmpty()){ + FiberState next = fibers.deQueue(); + switch(next.f resume next.reason){ + case pause: + fibers.push(FiberState{f:next,reason:go_ahead}); + break; + case async_request:{ + delayed.put(next); + break; + } + } + if(fibers.isEmpty() || pausing){ + switch(scheduler suspend global_pause){ + case io_notify(client,data):{ + reschedule(client,data); + } + } } } - if(fibers.isEmpty() || pausing) - return; } } ``` -The vast majority of the code for this scheduler is _boilerplate_ code that is manipulating data structures. We leave it as an exercise for the reader to translate it into WebAssembly. #### Reporting a paused execution The final piece of our scenario involves arranging for the WebAssembly application itself to pause so that browser's eventloop can complete the I/O operations and cause our code to be reentered. This is handled at the point where the WebAssembly is initially entered—i.e., through one of its exports. For the sake of exposition, we shall assume that we have a single export: `main`, which simply starts the scheduler: ``` -void main(){ - startScheduler(); +void inner_main(fiber scheduler){ + startScheduler(scheduler); } ``` -The interesting logic is actually in the JavaScript glue code that will be invoked by the wider JavaScript application: -``` -var P = Promise.race(currentFetches).then((K)=> { - // We remove winning fetch from currentFetches ... - currentFetches[K.fiber] = undefined; - restart_scheduler(K.fiber,K.txt); -}); -``` -where `restart_scheduler` is a new auxilliary export that allows the top-level scheduler to resume a fiber with the appropriate text: +In fact, however, our application itself should be compatible with the overall browser architecture. This means that our actual toplevel function returns a `Promise`: ``` -void restart_scheduler(int fiberNo,string txt){ - Fiber next = delayed.get(fiberNo); - delayed.delete(fiberNo); - - fibers.push(FiberState{f:next,reason:reason:io_result(Txt)}); - start_scheduler(); +function outer_main() { + return new Promise((resolve,reject) => { + spawn Fiber((F) => { + try{ + resolve(inner_main(F)); + } catch (E) { + reject(E); + } + }); + } } ``` -We should not that this may not be the most efficient way of managing the collection of `Promise`s. In particular, if operations such as `Promise.race` involve linear scans of the `currentFetches` list, then we risk being quadratic in the number of outstanding fetches. However, this example is primarily intended to explain how one might implement the integration between WebAPIs such as `fetch` and a `Fiber` aware programming language. +We should not claim that this is the only way of managing the asynchronous activities; indeed, indiviual language toolchains will have their own language specific requirements. However, this example is primarily intended to explain how one might implement the integration between WebAPIs such as `fetch` and a `Fiber` aware programming language. + +#### The importance of stable identifiers +One of the hallmarks of this example is the need to keep track of the identities of different computations; possibly over extended periods of time and across significant _code distances_. + +For example, we have to connect the scheduler to the import in order to ensure correct rescheduling of client code. At the time that we set up the callback to the `Promise` returned by `fetch` we reference the `scheduler` fiber. However, at that moment in time, the `scheduler` fiber is still technically running (i.e., it is not suspended). Of course, when the callback is invoked by the event loop the scheduler is suspended. + +This correlation is only possible because the identity of a fiber is stable‐regardless of its current execution state. + +#### Final note + +Finally, the vast majority of the code for this scheduler is _boilerplate_ code that is manipulating data structures. We leave it as an exercise for the reader to translate it into WebAssembly. ## Frequently Asked Questions From e9968ef654feb69ce919fee1a1695e53623e0b80 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Mon, 12 Sep 2022 08:36:38 -0700 Subject: [PATCH 7/7] Renamed the folder --- proposals/{tasks => fibers}/Explainer.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{tasks => fibers}/Explainer.md (100%) diff --git a/proposals/tasks/Explainer.md b/proposals/fibers/Explainer.md similarity index 100% rename from proposals/tasks/Explainer.md rename to proposals/fibers/Explainer.md