Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
| 0x2b 0x00 => (canon thread.yield-then-resume (core func)) 🧵
| 0x2c 0x00 => (canon thread.suspend-then-promote (core func)) 🧵
| 0x2d 0x00 => (canon thread.yield-then-promote (core func)) 🧵
| 0x30 => (canon thread.set-task (core func)) 🧵
| 0x31 => (canon thread.get-task (core func)) 🧵
| 0x32 => (canon task.drop (core func)) 🧵
| 0x40 sh?:<sh?> ft:<core:typeidx> => (canon thread.spawn-ref sh? ft (core func)) 🧵②
| 0x41 sh?:<sh?> ft:<core:typeidx> t:<core:tableidx> => (canon thread.spawn-indirect sh? ft t (core func)) 🧵②
| 0x42 sh?:<sh?> => (canon thread.available-parallelism sh? (core func)) 🧵②
Expand Down
227 changes: 144 additions & 83 deletions design/mvp/CanonicalABI.md

Large diffs are not rendered by default.

67 changes: 44 additions & 23 deletions design/mvp/Concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,16 @@ Thread
where a **component store** is the top-level "thing" and analogous to a Core
WebAssembly [store].

The reason for the thread/task split is that, when one thread creates a new
thread, the new thread is contained by the task of the original thread which
creates an N:1 relationship between threads and tasks that ties N threads to
the original export call (= "task") that transitively spawned those N threads.
This relationship serves several purposes described in the following sections.
When a component export is called, one new task is created for the call; this
task contains one new *implicit* thread that executes it. The reason for the
thread/task split is that this implicit thread may then go on to spawn N more
*explicit* threads (via `thread.new-indirect`) that are initially contained by
the same task, thereby creating an N:1 relationship between threads and tasks.

While the store:instance and instance:task relationships are immutably set on
creation, the task:thread relationship is *mutable*: guest code running inside a
component instance can change which task a thread is currently executing on
behalf of by calling the [`thread.set-task`] built-in, as described below.

In the Canonical ABI explainer, threads, tasks, component instances and
component stores are represented by the [`Thread`], [`Task`],
Expand Down Expand Up @@ -311,9 +316,15 @@ supertask, they can be thought of as a single node in the async call stack.

A subtask/supertask relationship is immutably established when an import is
called, setting the [current task](#current-thread-and-task) as the supertask
of the new subtask created for the import call. Thus, one reason for
associating every thread with a "containing task" is to ensure that there is
always a well-defined async call stack.
of the new subtask created for the import call. Thus, one reason for associating
every thread with a "containing task" is to ensure that there is always a
well-defined async call stack. Note that guest code can call [`thread.set-task`]
to change the containing task of a thread and thus the async call stack can
change completely between two program points while executing a single thread.
For example, when a JS runtime flushes its microtask queue and encounters a JS
callback associated with a task other than the current thread's task, the JS
runtime would call `thread.set-task` so that the async call stack matches the JS
developer's expectation.

The async call stack is not currently observable to running components, except
that it may nondeterministically appear as part of the callstack stored in
Expand All @@ -336,9 +347,9 @@ not enforcing a stricter form of Structured Concurrency at the Component Model
level is that there are important use cases where forcing a supertask's thread
to stay resident just to wait for subtasks to finish would waste resources
without tangible benefit. Instead, we can say that once a supertask's last
thread finishes execution, the supertask semantically "tail calls" any still-
executing subtasks, staying technically-alive and on the async call stack until
they complete, but not consuming real resources.
thread exits or switches to another task, the supertask semantically "tail
calls" any still-executing subtasks, staying technically-alive and on the
async call stack until they complete, but not consuming real resources.

For scenarios where one component wants to *non-cooperatively* put an upper
bound on execution of a call into another component, a separate "[blast zone]"
Expand Down Expand Up @@ -374,13 +385,23 @@ New threads are created with the [`thread.new-indirect`] built-in. As mentioned
[above](#threads-and-tasks), a spawned thread inherits the task of the spawning
thread which is why threads and tasks are N:1. `thread.new-indirect` adds a new
thread to the component instance's threads table and returns the `i32` index of
this table entry to the Core WebAssembly caller. Like [`pthread_create`],
`thread.new-indirect` takes a Core WebAssembly function (via index into a
`funcref` table) and a "closure" parameter to pass to the function when called
on the new thread. However, unlike `pthread_create`, the new thread is
initially in a "suspended" state and must be explicitly "resumed" using one of
the following 3 thread built-ins. Once the thread is resumed, the thread can
learn its own index by calling the [`thread.index`] built-in.
this table entry to the Core WebAssembly caller.

After creation, the implicitly-set containing task of a thread can be explicitly
overridden using the [`thread.set-task`] built-in. `thread.set-task` sets the
containing task of the current thread to a task handle that was retrieved via
[`thread.get-task`]. `thread.get-task` always allocates a fresh handle storing a
reference to the current thread's containing task. The `i32` index of this new
handle must later be explicitly dropped via [`task.drop`] to avoid leaking
the task. Tasks are thus kept alive by any or all of: contained threads, subtask
handles and task handles.

Like [`pthread_create`], `thread.new-indirect` takes a Core WebAssembly function
(via index into a `funcref` table) and a "closure" parameter to pass to the
function when called on the new thread. However, unlike `pthread_create`, the
new thread is initially in a "suspended" state and must be explicitly "resumed"
using one of the following 3 thread built-ins. Once the thread is resumed, the
thread can learn its own index by calling the [`thread.index`] built-in.

A suspended thread (identified by thread-table index) can be resumed at some
nondeterministic point in future via the [`thread.resume-later`] built-in. In
Expand Down Expand Up @@ -733,10 +754,7 @@ the "started" state.
The way an `async` export returns its value using the async ABI is by calling
[`task.return`], passing the core values that are to be lifted as *parameters*.
When using the async ABI, *any* of the threads contained by a task can call
`task.return`; there is no "main thread" of a task. When the last thread of a
task returns, there is a trap if `task.return` has not been called. Thus, *some*
thread (either the thread created implicitly for the initial export call or some
thread transitively created by that thread) must call `task.return`.
`task.return`; there is no "main thread" of a task.

Returning values by calling `task.return` allows a task to continue executing
even after it has passed its initial results to the caller. This is also
Expand All @@ -752,7 +770,7 @@ the readable end passed for `in`) and `stream.write`s (of the writable end it
`stream.new`ed) before exiting the task.

Once `task.return` is called, the task is in the "returned" state. Calling
`task.return` when not in the "started" state traps.
`task.return` when already in a resolved state traps.

### Borrows

Expand Down Expand Up @@ -1564,6 +1582,9 @@ the concurrency story:
[`thread.yield-then-resume`]: Explainer.md#-threadyield-then-resume
[`thread.suspend-then-promote`]: Explainer.md#-threadsuspend-then-promote
[`thread.yield-then-promote`]: Explainer.md#-threadyield-then-promote
[`thread.set-task`]: Explainer.md#-threadset-task
[`thread.get-task`]: Explainer.md#-threadget-task
[`task.drop`]: Explainer.md#-taskdrop
[`{stream,future}.new`]: Explainer.md#-streamnew-and-futurenew
[`{stream,future}.{read,write}`]: Explainer.md#-streamread-and-streamwrite
[`stream.cancel-write`]: Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write
Expand Down
63 changes: 62 additions & 1 deletion design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -1593,6 +1593,9 @@ canon ::= ...
| (canon thread.yield-then-resume (core func <id>?)) 🧵
| (canon thread.suspend-then-promote (core func <id>?)) 🧵
| (canon thread.yield-then-promote (core func <id>?)) 🧵
| (canon thread.set-task (core func <id>?)) 🧵
| (canon thread.get-task (core func <id>?)) 🧵
| (canon task.drop (core func <id>?)) 🧵
| (canon error-context.new <canonopt>* (core func <id>?)) 📝
| (canon error-context.debug-message <canonopt>* (core func <id>?)) 📝
| (canon error-context.drop (core func <id>?)) 📝
Expand Down Expand Up @@ -1748,7 +1751,7 @@ For details, see [Backpressure] in the concurrency explainer and
| Canonical ABI signature | `[lower(FuncT.results)*] -> []` |

The `task.return` built-in takes as parameters the result values of the
[current task]. One of `task.return` or `task.cancel` must be called exactly
[current task]. One of `task.return` or `task.cancel` must be called at most
once from any of a task's threads.

The `canon task.return` definition takes component-level return type and the
Expand Down Expand Up @@ -2320,6 +2323,60 @@ returned `i32` is always `0` and may be removed in a future ABI revision.
For details, see [Thread Built-ins] in the concurrency explainer and
[`canon_thread_yield_then_promote`] in the Canonical ABI explainer.

###### 🧵 `thread.set-task`

| Synopsis | |
| -------------------------- | --------------- |
| Approximate WIT signature | `func(t: task)` |
| Canonical ABI signature | `[t:i32] -> []` |

The `thread.set-task` built-in allows the [current thread] to set its containing
task to the given operand, which immediately changes the [current task].
Built-ins like `task.return` and `task.cancel` are defined in terms of "the
current task", and thus `thread.set-task` allows a thread to return a value for
a task other than the task that initially spawned the thread.

Tasks form an [async call stack] that is consulted for debugging, observability,
and host import-to-export call attribution. Thus, changing the current task
allows a guest's concurrency runtime to control what async call stack to
associate with the current wasm execution.

The `i32` index passed to `thread.set-task` must be a task handle which can
currently only be retrieved by calling `thread.get-task`.

For details, see [Thread Built-ins] in the concurrency explainer and
[`canon_thread_set_task`] in the Canonical ABI explainer.

###### 🧵 `thread.get-task`

| Synopsis | |
| -------------------------- | ---------------- |
| Approximate WIT signature | `func() -> task` |
| Canonical ABI signature | `[] -> [i32]` |

The `thread.get-task` built-in returns a handle referring to the [current task]
(which is the containing task of the [current thread]). This handle is currently
only used as an argument in `thread.set-task`. Each call to `thread.get-task`
returns a fresh handle which must be released by `task.drop` to avoid a handle
table leak.

For details, see [Thread Built-ins] in the concurrency explainer and
[`canon_thread_get_task`] in the Canonical ABI explainer.

###### 🧵 `task.drop`

| Synopsis | |
| -------------------------- | --------------- |
| Approximate WIT signature | `func(t: task)` |
| Canonical ABI signature | `[t:i32] -> []` |

The `task.drop` built-in drops a handle allocated by `thread.get-task`. The
state of the underlying task is not modified nor is the task destroyed, as it
may have other active referents.

For details, see [Thread Built-ins] in the concurrency explainer and
[`canon_task_drop`] in the Canonical ABI explainer.

###### 🧵② `thread.spawn-ref`

| Synopsis | |
Expand Down Expand Up @@ -3474,6 +3531,9 @@ For some use-case-focused, worked examples, see:
[`canon_thread_yield_then_resume`]: CanonicalABI.md#-canon-threadyield-then-resume
[`canon_thread_suspend_then_promote`]: CanonicalABI.md#-canon-threadsuspend-then-promote
[`canon_thread_yield_then_promote`]: CanonicalABI.md#-canon-threadyield-then-promote
[`canon_thread_get_task`]: CanonicalABI.md#-canon-threadget-task
[`canon_thread_set_task`]: CanonicalABI.md#-canon-threadset-task
[`canon_task_drop`]: CanonicalABI.md#-canon-taskdrop
[`canon_thread_spawn_ref`]: CanonicalABI.md#-canon-threadspawn-ref
[`canon_thread_spawn_indirect`]: CanonicalABI.md#-canon-threadspawn-indirect
[`canon_thread_available_parallelism`]: CanonicalABI.md#-canon-threadavailable_parallelism
Expand All @@ -3483,6 +3543,7 @@ For some use-case-focused, worked examples, see:
[Summary]: Concurrency.md#summary
[Current Thread]: Concurrency.md#current-thread-and-task
[Current Task]: Concurrency.md#current-thread-and-task
[Async Call Stack]: Concurrency.md#subtasks-and-supertasks
[Thread-Local Storage]: Concurrency.md#thread-local-storage
[Subtask]: Concurrency.md#subtasks-and-supertasks
[Stream or Future]: Concurrency.md#streams-and-futures
Expand Down
Loading
Loading