From a69f22ec26dcc270dbf4c5af34b3fc1ad9be41e5 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 7 Feb 2025 10:20:18 -0800 Subject: [PATCH] Add `thread.spawn_indirect` This change codifies the conclusions we arrived to in [#89]. It adds a new way to spawn threads, `thread.spawn_indirect`, which retrieves the thread start function from a table. This prompted me to rename `thread.spawn` to `thread.spawn_ref`. [#89]: https://github.com/WebAssembly/shared-everything-threads/issues/89 --- design/mvp/Async.md | 27 +++++++++++---------- design/mvp/Binary.md | 5 ++-- design/mvp/CanonicalABI.md | 48 +++++++++++++++++++++++++++++++++++--- design/mvp/Explainer.md | 33 +++++++++++++++++++------- 4 files changed, 86 insertions(+), 27 deletions(-) diff --git a/design/mvp/Async.md b/design/mvp/Async.md index 5aecfeee..bd8b4eea 100644 --- a/design/mvp/Async.md +++ b/design/mvp/Async.md @@ -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 @@ -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). @@ -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 @@ -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 diff --git a/design/mvp/Binary.md b/design/mvp/Binary.md index aba39655..a578c048 100644 --- a/design/mvp/Binary.md +++ b/design/mvp/Binary.md @@ -286,9 +286,10 @@ canon ::= 0x00 0x00 f: opts: ft: => (canon lift | 0x01 0x00 f: opts: => (canon lower f opts (core func)) | 0x02 rt: => (canon resource.new rt (core func)) | 0x03 rt: => (canon resource.drop rt (core func)) - | 0x07 rt: => (canon resource.drop rt async (core func)) + | 0x07 rt: => (canon resource.drop rt async (core func)) | 0x04 rt: => (canon resource.rep rt (core func)) - | 0x05 ft: => (canon thread.spawn ft (core func)) 🧵 + | 0x05 ft: => (canon thread.spawn_ref ft (core func)) 🧵 + | 0x24 ft: t: => (canon thread.spawn_indirect ft (table t) (core func)) 🧵 | 0x06 => (canon thread.available_parallelism (core func)) 🧵 | 0x08 => (canon backpressure.set (core func)) 🔀 | 0x09 rs: opts: => (canon task.return rs opts (core func)) 🔀 diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index f053fa0f..a16dd4d9 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -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 @@ -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] @@ -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) +* `$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: diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 479de769..26af1fde 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -1438,7 +1438,8 @@ canon ::= ... | (canon error-context.new * (core func ?)) | (canon error-context.debug-message * (core func ?)) | (canon error-context.drop (core func ?)) - | (canon thread.spawn (core func ?)) 🧵 + | (canon thread.spawn_ref (core func ?)) 🧵 + | (canon thread.spawn_indirect (core func ?)) 🧵 | (canon thread.available_parallelism (core func ?)) 🧵 ``` @@ -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(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(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` @@ -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