Skip to content
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

Add thread.spawn_indirect #447

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
27 changes: 13 additions & 14 deletions design/mvp/Async.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -630,11 +629,11 @@ these values is defined by the Canonical ABI.

## 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).

Expand All @@ -651,8 +650,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
Expand Down Expand Up @@ -720,7 +719,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#-threadspawnref
[ESM-integration]: Explainer.md#ESM-integration

[Canonical ABI Explainer]: CanonicalABI.md
Expand Down
5 changes: 3 additions & 2 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,10 @@ 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)) 🧵
| 0x05 ft:<typeidx> => (canon thread.spawn_ref ft (core func)) 🧵
| 0x24 ft:<typeidx> t:<core:tabidx> => (canon thread.spawn_indirect ft (table t) (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)) 🔀
Expand Down
48 changes: 45 additions & 3 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -3761,11 +3761,11 @@ 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 (type $ft) (core func $st))
```
validation specifies:
* `$ft` must refer to a `shared` function type; initially, only the type `(func
Expand All @@ -3790,7 +3790,7 @@ thread which:
In pseudocode, `$st` looks like:

```python
def canon_thread_spawn(f, c):
def canon_thread_spawn_ref(f, c):
trap_if(f is None)
if DETERMINISTIC_PROFILE:
return [-1]
Expand All @@ -3808,6 +3808,48 @@ def canon_thread_spawn(f, c):
```


### 🧵 `canon thread.spawn_indirect`

For a canonical definition:
```wat
(canon thread.spawn_indirect (type $ft) (table $t) (core func $st))
```
validation specifies:
* `$ft` must refer to a `shared` function type; initially, only the type `(func
shared (param $c i32))` is allowed (see explanation in `thread.spawn_ref`
above)
Comment on lines +3818 to +3820
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I can't remember well is whether we truly need $ft in the definition. I left it in for now, but it would seem that without it we can still statically verify from the type of $t that the table elements are in fact shared function references.

Copy link
Contributor Author

@abrown abrown Feb 7, 2025

Choose a reason for hiding this comment

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

Implementation-wise it is $ft that is problematic currently, so at least temporarily it isn't too useful anyways seeing as we must fix the start function to only [i32] -> [].

* `$t` must refer to a table containing `ft`-typed items
* `$st` is given type `(func (param $i i32) (param $c i32) (result $e
i32))`.

Calling `$st` retrieves a function `$f` of type `$ft` from table `$t`. 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:

```python
def canon_thread_spawn_indirect(t, i, c):
trap_if(t[i] is None)
f = t[i]
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.available_parallelism`

For a canonical definition:
Expand Down
33 changes: 25 additions & 8 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>?)) 🧵
```

Expand Down Expand Up @@ -1946,19 +1947,34 @@ 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]` |

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: i32, 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` (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`

Expand Down Expand Up @@ -2807,7 +2823,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-threadspawnref
[`canon_thread_spawn_indirect`]: CanonicalABI.md#-canon-threadspawnindirect
[`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
Expand Down