Skip to content

Commit 35db146

Browse files
committed
CABI: improve and add cooperative thread built-ins
1 parent a714897 commit 35db146

6 files changed

Lines changed: 712 additions & 269 deletions

File tree

design/mvp/CanonicalABI.md

Lines changed: 231 additions & 157 deletions
Large diffs are not rendered by default.

design/mvp/Concurrency.md

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ concurrency-specific goals and use cases:
6565

6666
## Summary
6767

68+
TODO: re-read and update?
69+
6870
To support the wide variety of language-level concurrency mechanisms listed
6971
above, the Component Model defines a new low-level, language-agnostic async
7072
calling convention (the "async ABI") for both calling into and calling out of
@@ -386,34 +388,37 @@ initially in a "suspended" state and must be explicitly "resumed" using one of
386388
the following 3 thread built-ins. Once the thread is resumed, the thread can
387389
learn its own index by calling the [`thread.index`] built-in.
388390

391+
TODO: find a place to put [`thread.exit`]
392+
TODO: work in [`thread.{suspend,yield}-then-promote`] below
393+
389394
A suspended thread (identified by thread-table index) can be resumed at some
390395
nondeterministic point in future via the [`thread.resume-later`] built-in. In
391-
contrast, the [`thread.yield-to`] built-in switches execution to the given
392-
thread immediately, leaving the *calling* thread to be resumed at some
393-
nondeterministic point in the future. Lastly, the [`thread.switch-to`]
394-
built-in switches execution to the given thread immediately, like `yield-to`,
395-
but leaves the calling thread in the "suspended" state. These three functions
396-
can be used to resume both newly-created threads as well as threads that
397-
executed and then suspended.
396+
contrast, the [`thread.yield-then-resume`] built-in switches execution to the
397+
given thread immediately, leaving the *calling* thread to be resumed at some
398+
nondeterministic point in the future. Lastly, the [`thread.suspend-then-resume`]
399+
built-in switches execution to the given thread immediately, like
400+
`thread.yield-then-resume`, but leaves the calling thread in the "suspended"
401+
state. These three functions can be used to resume both newly-created threads as
402+
well as threads that executed and then suspended.
398403

399404
In addition to threads entering the suspended state via `thread.new-indirect`
400-
and `thread.switch-to`, threads can also explicitly suspend themselves via the
401-
[`thread.suspend`] built-in. Thus, there are three ways a thread *enters* the
402-
suspended state and three ways a thread *exits* the suspended state (with
403-
`thread.switch-to` serving in both categories). Together, these 5 thread
404-
built-ins support both the "green thread" [use cases](#goals) where Core
405-
WebAssembly code running inside the component wants to fully control thread
406-
scheduling (via `thread.switch-to` and `thread.suspend`) as well as the "host
407-
thread" use cases where the Core WebAssembly code wants to let the containing
408-
runtime nondeterministically schedule threads (via `thread.resume-later` or
409-
`thread.yield-to`).
405+
and `thread.suspend-then-resume`, threads can also explicitly suspend themselves
406+
via the [`thread.suspend`] built-in. Thus, there are three ways a thread
407+
*enters* the suspended state and three ways a thread *exits* the suspended state
408+
(with `thread.suspend-then-resume` serving in both categories). Together, these
409+
5 thread built-ins support both the "green thread" [use cases](#goals) where
410+
Core WebAssembly code running inside the component wants to fully control thread
411+
scheduling (via `thread.suspend-then-resume` and `thread.suspend`) as well as
412+
the "host thread" use cases where the Core WebAssembly code wants to let the
413+
containing runtime nondeterministically schedule threads (via
414+
`thread.resume-later` or `thread.yield-then-resume`).
410415

411416
Lastly, since threads are cooperative, there is a [`thread.yield`] built-in
412417
that can be called in the middle of long-running computations to allow the
413418
runtime to nondeterministically switch execution to another thread.
414419
`thread.yield` is equivalent to (but obviously more efficient than) creating a
415420
new thread with a no-op function (via `thread.new-indirect`) and then yielding
416-
to it (via `thread.yield-to`).
421+
to it (via `thread.yield-then-resume`).
417422

418423
### Thread-Local Storage
419424

@@ -466,6 +471,8 @@ For more information, see [`context.get`] in the AST explainer.
466471

467472
### Blocking
468473

474+
TODO: this section has to be updated
475+
469476
When a thread calls an import using the async ABI, the Component Model
470477
guarantees that if the callee **blocks**, control flow is immediately returned
471478
back to the caller. When the callee is implemented by the *host*, what counts as
@@ -506,9 +513,6 @@ type does not contain `async`.
506513
that suspends the current thread to produce a continuation that can be resumed
507514
once the reason for blocking is addressed.
508515

509-
The [Canonical ABI explainer] defines the above behavior more precisely; search
510-
for `may_block` to see all the relevant points.
511-
512516
### Waitables and Waitable Sets
513517

514518
When an `async`-typed function is called with the async ABI and the call
@@ -678,12 +682,12 @@ degree of concurrency than synchronous exports. Stackful async exports ignore
678682
the lock entirely and thus achieve the highest degree of (cooperative)
679683
concurrency.
680684

681-
Since non-`async` functions are not allowed to block (including due to
682-
backpressure) and also don't pile up like `async` functions, non-`async`
683-
functions ignore backpressure (explicit and implicit) entirely. If a
684-
component exports a mix of `async` and non-`async` functions, code generation
685-
must therefore be prepared to handle non-`async` functions executing at
686-
any cooperative yield point, even in the middle of a `callback`.
685+
Since non-`async` functions are not allowed to [block](#blocking) (including due
686+
to backpressure) and also don't pile up like `async` functions, non-`async`
687+
functions ignore backpressure (explicit and implicit) entirely. If a component
688+
exports a mix of `async` and non-`async` functions, code generation must
689+
therefore be prepared to handle non-`async` functions executing at any
690+
cooperative yield point, even in the middle of a `callback`.
687691

688692
Once a task is allowed to start according to these backpressure rules, its
689693
arguments are lowered into the callee's linear memory and the task is in
@@ -713,9 +717,7 @@ the readable end passed for `in`) and `stream.write`s (of the writable end it
713717
`stream.new`ed) before exiting the task.
714718

715719
Once `task.return` is called, the task is in the "returned" state. Calling
716-
`task.return` when not in the "started" state traps. Once in a "returned" state,
717-
non-`async` functions may block using cooperative threads that were created
718-
before the synchronous task's implicit thread returned.
720+
`task.return` when not in the "started" state traps.
719721

720722
### Borrows
721723

@@ -1538,13 +1540,16 @@ comes after:
15381540
[`waitable-set.wait`]: Explainer.md#-waitable-setwait
15391541
[`waitable-set.poll`]: Explainer.md#-waitable-setpoll
15401542
[`waitable.join`]: Explainer.md#-waitablejoin
1541-
[`thread.new-indirect`]: Explainer.md#-threadnew-indirect
15421543
[`thread.index`]: Explainer.md#-threadindex
1543-
[`thread.suspend`]: Explainer.md#-threadsuspend
1544-
[`thread.switch-to`]: Explainer.md#-threadswitch-to
1544+
[`thread.exit`]: Explainer.md#-threadexit
1545+
[`thread.new-indirect`]: Explainer.md#-threadnew-indirect
15451546
[`thread.resume-later`]: Explainer.md#-threadresume-later
1546-
[`thread.yield-to`]: Explainer.md#-threadyield-to
1547+
[`thread.suspend`]: Explainer.md#-threadsuspend
15471548
[`thread.yield`]: Explainer.md#-threadyield
1549+
[`thread.suspend-then-resume`]: Explainer.md#-threadsuspend-then-resume
1550+
[`thread.yield-then-resume`]: Explainer.md#-threadyield-then-resume
1551+
[`thread.suspend-then-promote`]: Explainer.md#-threadsuspend-then-promote
1552+
[`thread.yield-then-promote`]: Explainer.md#-threadyield-then-promote
15481553
[`{stream,future}.new`]: Explainer.md#-streamnew-and-futurenew
15491554
[`{stream,future}.{read,write}`]: Explainer.md#-streamread-and-streamwrite
15501555
[`stream.cancel-write`]: Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write

design/mvp/Explainer.md

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,12 +1464,15 @@ canon ::= ...
14641464
| (canon future.drop-readable <typeidx> (core func <id>?)) 🔀
14651465
| (canon future.drop-writable <typeidx> (core func <id>?)) 🔀
14661466
| (canon thread.index (core func <id>?)) 🧵
1467+
| (canon thread.exit (core func <id>?)) 🧵
14671468
| (canon thread.new-indirect <typeidx> <core:tableidx> (core func <id>?)) 🧵
14681469
| (canon thread.resume-later (core func <id>?)) 🧵
14691470
| (canon thread.suspend cancellable? (core func <id>?)) 🧵
14701471
| (canon thread.yield cancellable? (core func <id>?)) 🔀
1471-
| (canon thread.switch-to cancellable? (core func <id>?)) 🧵
1472-
| (canon thread.yield-to cancellable? (core func <id>?)) 🧵
1472+
| (canon thread.suspend-then-resume cancellable? (core func <id>?)) 🧵
1473+
| (canon thread.yield-then-resume cancellable? (core func <id>?)) 🧵
1474+
| (canon thread.suspend-then-promote cancellable? (core func <id>?)) 🧵
1475+
| (canon thread.yield-then-promote cancellable? (core func <id>?)) 🧵
14731476
| (canon error-context.new <canonopt>* (core func <id>?)) 📝
14741477
| (canon error-context.debug-message <canonopt>* (core func <id>?)) 📝
14751478
| (canon error-context.drop (core func <id>?)) 📝
@@ -2067,6 +2070,13 @@ learn their index via `thread.index`.
20672070
For details, see [Thread Built-ins] in the concurrency explainer and
20682071
[`canon_thread_index`] in the Canonical ABI explainer.
20692072

2073+
###### 🧵 `thread.exit`
2074+
2075+
TODO
2076+
2077+
For details, see [Thread Built-ins] in the concurrency explainer and
2078+
[`canon_thread_exit`] in the Canonical ABI explainer.
2079+
20702080
###### 🧵 `thread.new-indirect`
20712081

20722082
| Synopsis | |
@@ -2089,7 +2099,7 @@ extended for [GC].
20892099

20902100
As explained in the [concurrency explainer], a thread created by
20912101
`thread.new-indirect` is initially in a suspended state and must be resumed
2092-
eagerly or lazily by [`thread.yield-to`](#-threadyield-to) or
2102+
eagerly or lazily by [`thread.yield-then-resume`](#-threadyield-then-resume) or
20932103
[`thread.resume-later`](#-threadresume-later), resp., to begin execution.
20942104

20952105
For details, see [Thread Built-ins] in the concurrency explainer and
@@ -2122,9 +2132,6 @@ explicitly resumed by some other thread calling a built-in such as
21222132
the current task was [cancelled] by the caller; otherwise, `thread.suspend`
21232133
always returns `false`.
21242134

2125-
A non-`async`-typed function export that has not yet returned a value traps if
2126-
it transitively attempts to call `thread.suspend`.
2127-
21282135
For details, see [Thread Built-ins] in the concurrency explainer and
21292136
[`canon_thread_suspend`] in the Canonical ABI explainer.
21302137

@@ -2138,52 +2145,59 @@ For details, see [Thread Built-ins] in the concurrency explainer and
21382145
The `thread.yield` built-in allows the runtime to potentially switch to any
21392146
other thread in the "ready" state, enabling a long-running computation to
21402147
cooperatively interleave execution without specifically requesting another
2141-
thread to be resumed (as with `thread.yield-to`). If `cancellable` is set,
2142-
`thread.yield` returns whether the current task was [cancelled] by the caller;
2143-
otherwise, `thread.yield` always returns `false`.
2144-
2145-
If `thread.yield` is called from a synchronous- or `async callback`-lifted
2146-
export, it returns immediately without blocking (instead of trapping, as with
2147-
other possibly-blocking operations like `waitable-set.wait`). This is because,
2148-
unlike other built-ins, `thread.yield` may be scattered liberally throughout
2149-
code that might show up in the transitive call tree of a synchronous function
2150-
call.
2148+
thread to be resumed (as with `thread.yield-then-resume`). If `cancellable` is
2149+
set, `thread.yield` returns whether the current task was [cancelled] by the
2150+
caller; otherwise, `thread.yield` always returns `false`.
21512151

21522152
For details, see [Thread Built-ins] in the concurrency explainer and
21532153
[`canon_thread_yield`] in the Canonical ABI explainer.
21542154

2155-
###### 🧵 `thread.switch-to`
2155+
###### 🧵 `thread.suspend-then-resume`
21562156

21572157
| Synopsis | |
21582158
| -------------------------- | --------------------------------------- |
21592159
| Approximate WIT signature | `func<cancellable?>(t: thread) -> bool` |
21602160
| Canonical ABI signature | `[t:i32] -> [i32]` |
21612161

2162-
The `thread.switch-to` built-in suspends the [current thread] and immediately
2163-
resumes execution of the thread `t`, trapping if `t` is not in a "suspended"
2164-
state. If `cancellable` is set, `thread.switch-to` returns whether the current
2165-
task was [cancelled] by the caller; otherwise, `thread.switch-to` always returns
2166-
`false`.
2162+
The `thread.suspend-then-resume` built-in suspends the [current thread] and
2163+
immediately resumes execution of the thread `t`, trapping if `t` is not in a
2164+
"suspended" state. If `cancellable` is set, `thread.suspend-then-resume` returns
2165+
whether the current task was [cancelled] by the caller; otherwise,
2166+
`thread.suspend-then-resume` always returns `false`.
21672167

21682168
For details, see [Thread Built-ins] in the concurrency explainer and
2169-
[`canon_thread_switch_to`] in the Canonical ABI explainer.
2169+
[`canon_thread_suspend_then_resume`] in the Canonical ABI explainer.
21702170

2171-
###### 🧵 `thread.yield-to`
2171+
###### 🧵 `thread.yield-then-resume`
21722172

21732173
| Synopsis | |
21742174
| -------------------------- | --------------------------------------- |
21752175
| Approximate WIT signature | `func<cancellable?>(t: thread) -> bool` |
21762176
| Canonical ABI signature | `[t:i32] -> [i32]` |
21772177

2178-
The `thread.yield-to` built-in immediately resumes execution of the thread `t`,
2179-
(trapping if `t` is not in a "suspended" state) leaving the [current thread] in
2180-
a "ready" state so that the runtime can nondeterministically resume the current
2181-
thread at some point in the future. If `cancellable` is set, `thread.yield-to`
2182-
returns whether the current task was [cancelled] by the caller; otherwise,
2183-
`thread.yield-to` always returns `false`.
2178+
The `thread.yield-then-resume` built-in immediately resumes execution of the
2179+
thread `t`, (trapping if `t` is not in a "suspended" state) leaving the [current
2180+
thread] in a "ready" state so that the runtime can nondeterministically resume
2181+
the current thread at some point in the future. If `cancellable` is set,
2182+
`thread.yield-then-resume` returns whether the current task was [cancelled] by
2183+
the caller; otherwise, `thread.yield-then-resume` always returns `false`.
2184+
2185+
For details, see [Thread Built-ins] in the concurrency explainer and
2186+
[`canon_thread_yield_then_resume`] in the Canonical ABI explainer.
2187+
2188+
###### 🧵 `thread.suspend-then-promote`
2189+
2190+
TODO
2191+
2192+
For details, see [Thread Built-ins] in the concurrency explainer and
2193+
[`canon_thread_suspend_then_promote`] in the Canonical ABI explainer.
2194+
2195+
###### 🧵 `thread.yield-then-promote`
2196+
2197+
TODO
21842198

21852199
For details, see [Thread Built-ins] in the concurrency explainer and
2186-
[`canon_thread_yield_to`] in the Canonical ABI explainer.
2200+
[`canon_thread_yield_then_promote`] in the Canonical ABI explainer.
21872201

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

@@ -3282,12 +3296,15 @@ For some use-case-focused, worked examples, see:
32823296
[`canon_error_context_debug_message`]: CanonicalABI.md#-canon-error-contextdebug-message
32833297
[`canon_error_context_drop`]: CanonicalABI.md#-canon-error-contextdrop
32843298
[`canon_thread_index`]: CanonicalABI.md#-canon-threadindex
3299+
[`canon_thread_exit`]: CanonicalABI.md#-canon-threadexit
32853300
[`canon_thread_new_indirect`]: CanonicalABI.md#-canon-threadnew-indirect
3286-
[`canon_thread_suspend`]: CanonicalABI.md#-canon-threadsuspend
3287-
[`canon_thread_switch_to`]: CanonicalABI.md#-canon-threadswitch-to
32883301
[`canon_thread_resume_later`]: CanonicalABI.md#-canon-threadresume-later
3289-
[`canon_thread_yield_to`]: CanonicalABI.md#-canon-threadyield-to
3302+
[`canon_thread_suspend`]: CanonicalABI.md#-canon-threadsuspend
32903303
[`canon_thread_yield`]: CanonicalABI.md#-canon-threadyield
3304+
[`canon_thread_suspend_then_resume`]: CanonicalABI.md#-canon-threadsuspend-then-resume
3305+
[`canon_thread_yield_then_resume`]: CanonicalABI.md#-canon-threadyield-then-resume
3306+
[`canon_thread_suspend_then_promote`]: CanonicalABI.md#-canon-threadsuspend-then-promote
3307+
[`canon_thread_yield_then_promote`]: CanonicalABI.md#-canon-threadyield-then-promote
32913308
[`canon_thread_spawn_ref`]: CanonicalABI.md#-canon-threadspawn-ref
32923309
[`canon_thread_spawn_indirect`]: CanonicalABI.md#-canon-threadspawn-indirect
32933310
[`canon_thread_available_parallelism`]: CanonicalABI.md#-canon-threadavailable_parallelism

0 commit comments

Comments
 (0)