Skip to content

Add thread.spawn_indirect #447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 28, 2025
Merged
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
27 changes: 13 additions & 14 deletions design/mvp/Async.md
Original file line number Diff line number Diff line change
@@ -78,12 +78,11 @@ these languages' concurrency features are already bound (making the Component
Model "just another OS" from the language toolchains' perspective).

Moreover, this async ABI does not require components to use preemptive
multi-threading ([`thread.spawn`]) in order to achieve concurrency. Instead,
concurrency can be achieved by cooperatively switching between different
logical tasks running on a single thread. This switching may require the use of
[fibers] or a [CPS transform], but may also be avoided entirely when a
component's producer toolchain is engineered to always return to an
[event loop].
multi-threading ([`thread.spawn*`]) in order to achieve concurrency. Instead,
concurrency can be achieved by cooperatively switching between different logical
tasks running on a single thread. This switching may require the use of [fibers]
or a [CPS transform], but may also be avoided entirely when a component's
producer toolchain is engineered to always return to an [event loop].

To avoid partitioning the world along sync/async lines as mentioned in the
Goals section, the Component Model allows *every* component-level function type
@@ -672,11 +671,11 @@ by declarative instantiation and `start` above.

## Interaction with multi-threading

For now, the integration between multi-threading (via [`thread.spawn`]) and
native async is limited. In particular, because all [lift and lower
definitions] produce non-`shared` functions, any threads spawned by a component
via `thread.spawn` will not be able to directly call imports (synchronously
*or* asynchronously) and will thus have to use Core WebAssembly `atomics.*`
For now, the integration between multi-threading (via [`thread.spawn*`]) and
native async is limited. In particular, because all [lift and lower definitions]
produce non-`shared` functions, any threads spawned by a component via
`thread.spawn*` will not be able to directly call imports (synchronously *or*
asynchronously) and will thus have to use Core WebAssembly `atomics.*`
instructions to switch back to a non-`shared` function running on the "main"
thread (i.e., whichever thread was used to call the component's exports).

@@ -693,8 +692,8 @@ composition story described above could naturally be extended to a
sync+async+shared composition story, continuing to avoid the "what color is
your function" problem (where `shared` is the [color]).

Even without any use of `thread.new`, native async provides an opportunity to
achieve some automatic parallelism "for free". In particular, due to the
Even without any use of [`thread.spawn*`], native async provides an opportunity
to achieve some automatic parallelism "for free". In particular, due to the
shared-nothing nature of components, each component instance could be given a
separate thread on which to interleave all tasks executing in that instance.
Thus, in a cross-component call from `C1` to `C2`, `C2`'s task can run in a
@@ -750,7 +749,7 @@ comes after:
[`yield`]: Explainer.md#-yield
[`waitable-set.wait`]: Explainer.md#-waitable-setwait
[`waitable-set.poll`]: Explainer.md#-waitable-setpoll
[`thread.spawn`]: Explainer.md#-threadspawn
[`thread.spawn*`]: Explainer.md#-threadspawn_ref
[ESM-integration]: Explainer.md#ESM-integration

[Canonical ABI Explainer]: CanonicalABI.md
7 changes: 4 additions & 3 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
@@ -286,10 +286,8 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
| 0x01 0x00 f:<funcidx> opts:<opts> => (canon lower f opts (core func))
| 0x02 rt:<typeidx> => (canon resource.new rt (core func))
| 0x03 rt:<typeidx> => (canon resource.drop rt (core func))
| 0x07 rt:<typdidx> => (canon resource.drop rt async (core func)) 🔀
| 0x07 rt:<typeidx> => (canon resource.drop rt async (core func)) 🔀
| 0x04 rt:<typeidx> => (canon resource.rep rt (core func))
| 0x05 ft:<typeidx> => (canon thread.spawn ft (core func)) 🧵
| 0x06 => (canon thread.available_parallelism (core func)) 🧵
| 0x08 => (canon backpressure.set (core func)) 🔀
| 0x09 rs:<resultlist> opts:<opts> => (canon task.return rs opts (core func)) 🔀
| 0x0a 0x7f i:<u32> => (canon context.get i32 i (core func)) 🔀
@@ -318,6 +316,9 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
| 0x21 async?:<async>? m:<core:memidx> => (canon waitable-set.poll async? (memory m) (core func)) 🔀
| 0x22 => (canon waitable-set.drop (core func)) 🔀
| 0x23 => (canon waitable.join (core func)) 🔀
| 0x40 ft:<typeidx> => (canon thread.spawn_ref ft (core func)) 🧵
| 0x41 ft:<typeidx> tbl:<core:tableidx> => (canon thread.spawn_indirect ft tbl (core func)) 🧵
| 0x42 => (canon thread.available_parallelism (core func)) 🧵
async? ::= 0x00 =>
| 0x01 => async
opts ::= opt*:vec(<canonopt>) => opt*
72 changes: 59 additions & 13 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
@@ -54,7 +54,8 @@ being specified here.
* [`canon error-context.new`](#-canon-error-contextnew) 🔀
* [`canon error-context.debug-message`](#-canon-error-contextdebug-message) 🔀
* [`canon error-context.drop`](#-canon-error-contextdrop) 🔀
* [`canon thread.spawn`](#-canon-threadspawn) 🧵
* [`canon thread.spawn_ref`](#-canon-threadspawn_ref) 🧵
* [`canon thread.spawn_indirect`](#-canon-threadspawn_indirect) 🧵
* [`canon thread.available_parallelism`](#-canon-threadavailable_parallelism) 🧵

## Supporting definitions
@@ -3773,37 +3774,82 @@ async def canon_error_context_drop(task, i):
```


### 🧵 `canon thread.spawn`
### 🧵 `canon thread.spawn_ref`

For a canonical definition:
```wat
(canon thread.spawn (type $ft) (core func $st))
(canon thread.spawn_ref $ft (core func $spawn_ref))
```
validation specifies:
* `$ft` must refer to a `shared` function type; initially, only the type `(func
shared (param $c i32))` is allowed (see explanation below)
* `$st` is given type `(func (param $f (ref null $ft)) (param $c i32) (result $e
i32))`.
* `$ft` must refer to a `shared` function type; initially, only the type
`(shared (func (param $c i32)))` is allowed (see explanation below)
* `$spawn_ref` is given type `(func (param $f (ref null $ft)) (param $c i32)
(result $e i32))`.

> Note: ideally, a thread could be spawned with [arbitrary thread parameters].
> Currently, that would require additional work in the toolchain to support so,
> for simplicity, the current proposal simply fixes a single `i32` parameter type.
> However, `thread.spawn` could be extended to allow arbitrary thread parameters
> in the future, once it's concretely beneficial to the toolchain.
> for simplicity, the current proposal simply fixes a single `i32` parameter
> type. However, `thread.spawn_ref` could be extended to allow arbitrary thread
> parameters in the future, once it's concretely beneficial to the toolchain.
> The inclusion of `$ft` ensures backwards compatibility for when arbitrary
> parameters are allowed.

Calling `$st` checks that the reference `$f` is not null. Then, it spawns a
Calling `$spawn_ref` checks that the reference `$f` is not null. Then, it spawns
a thread which:
- invokes `$f` with `$c`
- executes `$f` until completion or trap in a `shared` context as described by
the [shared-everything threads] proposal.

In pseudocode, `$spawn_ref` looks like:

```python
def canon_thread_spawn_ref(f, c):
trap_if(f is None)
if DETERMINISTIC_PROFILE:
return [-1]

def thread_start():
try:
f(c)
except CoreWebAssemblyException:
trap()

if spawn(thread_start):
return [0]
else:
return [-1]
```


### 🧵 `canon thread.spawn_indirect`

For a canonical definition:
```wat
(canon thread.spawn_indirect $ft $tbl (core func $spawn_indirect))
```
validation specifies:
* `$ft` must refer to a `shared` function type; initially, only the type
`(shared (func (param $c i32)))` is allowed (see explanation in
`thread.spawn_ref` above)
* `$tbl` must refer to a table with type `(table (ref null (shared func))
shared)`
Comment on lines +3834 to +3835
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table type doesn't seem to follow any syntax defined in the proposal. More importantly, we have subtyping. So I think this ought to say:

* `$tbl` must refer to a shared table whose element type matches `(ref null (shared func))`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks, good point!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in #462.

* `$spawn_indirect` is given type `(func (param $i i32) (param $c i32) (result
$e i32))`.

Calling `$spawn_indirect` retrieves a reference to function `$f` from table
`$tbl` and checks that `$f` is of type `$ft`. If that succeeds, it spawns a
thread which:
- invokes `$f` with `$c`
- executes `$f` until completion or trap in a `shared` context as described by
the [shared-everything threads] proposal.

In pseudocode, `$st` looks like:
In pseudocode, `$spawn_indirect` looks like:

```python
def canon_thread_spawn(f, c):
def canon_thread_spawn_indirect(ft, tbl, i, c):
f = tbl[i]
trap_if(f is None)
trap_if(f.type != ft)
if DETERMINISTIC_PROFILE:
return [-1]

49 changes: 34 additions & 15 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
@@ -1438,7 +1438,8 @@ canon ::= ...
| (canon error-context.new <canonopt>* (core func <id>?))
| (canon error-context.debug-message <canonopt>* (core func <id>?))
| (canon error-context.drop (core func <id>?))
| (canon thread.spawn <typeidx> (core func <id>?)) 🧵
| (canon thread.spawn_ref <typeidx> (core func <id>?)) 🧵
| (canon thread.spawn_indirect <typeidx> <core:tableidx> (core func <id>?)) 🧵
| (canon thread.available_parallelism (core func <id>?)) 🧵
```

@@ -1945,19 +1946,35 @@ thread management. These are specified as built-ins and not core WebAssembly
instructions because browsers expect this functionality to come from existing
Web/JS APIs.

###### 🧵 `thread.spawn`
###### 🧵 `thread.spawn_ref`

| Synopsis | |
| -------------------------- | --------------------------------------------------------- |
| Approximate WIT signature | `func<FuncT>(f: FuncT, c: FuncT.params[0]) -> bool` |
| Canonical ABI signature | `[f:(ref null (func shared (param i32))) c:i32] -> [i32]` |
| Synopsis | |
| -------------------------- | ---------------------------------------------------------- |
| Approximate WIT signature | `func<FuncT>(f: FuncT, c: FuncT.params[0]) -> bool` |
| Canonical ABI signature | `[f:(ref null (shared (func (param i32))) c:i32] -> [i32]` |

The `thread.spawn` built-in spawns a new thread by invoking the shared function
`f` while passing `c` to it, returning whether a thread was successfully
spawned. While it's designed to allow different types in the future, the type
of `c` is currently hard-coded to always be `i32`.
The `thread.spawn_ref` built-in spawns a new thread by invoking the shared
function `f` while passing `c` to it, returning whether a thread was
successfully spawned. While it's designed to allow different types in the
future, the type of `c` is currently hard-coded to always be `i32`.

(See also [`canon_thread_spawn`] in the Canonical ABI explainer.)
(See also [`canon_thread_spawn_ref`] in the Canonical ABI explainer.)


###### 🧵 `thread.spawn_indirect`

| Synopsis | |
| -------------------------- | ------------------------------------------------- |
| Approximate WIT signature | `func<FuncT>(i: u32, c: FuncT.params[0]) -> bool` |
| Canonical ABI signature | `[i:i32 c:i32] -> [i32]` |

The `thread.spawn_indirect` built-in spawns a new thread by retrieving the
shared function `f` from a table using index `i` and traps if the type of `f` is
not equal to `FuncT` (much like the `call_indirect` core instruction). Once `f`
is retrieved, this built-in operates like `thread.spawn_ref` above, including
the limitations on `f`'s parameters.

(See also [`canon_thread_spawn_indirect`] in the Canonical ABI explainer.)

###### 🧵 `thread.available_parallelism`

@@ -1966,14 +1983,15 @@ of `c` is currently hard-coded to always be `i32`.
| Approximate WIT signature | `func() -> u32` |
| Canonical ABI signature | `[] -> [i32]` |

The `thread.available_parallelism` built-in returns the number of threads that can be
expected to execute in parallel.
The `thread.available_parallelism` built-in returns the number of threads that
can be expected to execute in parallel.

The concept of "available parallelism" corresponds is sometimes referred to
as "hardware concurrency", such as in [`navigator.hardwareConcurrency`] in
JavaScript.

(See also [`canon_thread_available_parallelism`] in the Canonical ABI explainer.)
(See also [`canon_thread_available_parallelism`] in the Canonical ABI
explainer.)

### 🪙 Value Definitions

@@ -2806,7 +2824,8 @@ For some use-case-focused, worked examples, see:
[`canon_error_context_new`]: CanonicalABI.md#-canon-error-contextnew
[`canon_error_context_debug_message`]: CanonicalABI.md#-canon-error-contextdebug-message
[`canon_error_context_drop`]: CanonicalABI.md#-canon-error-contextdrop
[`canon_thread_spawn`]: CanonicalABI.md#-canon-theadspawn
[`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
[`pack_async_copy_result`]: CanonicalABI.md#-canon-streamfuturereadwrite
[the `close` built-ins]: CanonicalABI.md#-canon-streamfutureclose-readablewritable