Skip to content

Commit 70ef870

Browse files
committed
CABI: improve and add cooperative thread built-ins
1 parent a0a9398 commit 70ef870

6 files changed

Lines changed: 707 additions & 290 deletions

File tree

design/mvp/CanonicalABI.md

Lines changed: 193 additions & 155 deletions
Large diffs are not rendered by default.

design/mvp/Concurrency.md

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -388,32 +388,43 @@ learn its own index by calling the [`thread.index`] built-in.
388388

389389
A suspended thread (identified by thread-table index) can be resumed at some
390390
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.
398-
399-
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`).
410-
411-
Lastly, since threads are cooperative, there is a [`thread.yield`] built-in
412-
that can be called in the middle of long-running computations to allow the
413-
runtime to nondeterministically switch execution to another thread.
414-
`thread.yield` is equivalent to (but obviously more efficient than) creating a
415-
new thread with a no-op function (via `thread.new-indirect`) and then yielding
416-
to it (via `thread.yield-to`).
391+
contrast, the [`thread.yield-then-resume`] built-in switches execution to the
392+
given thread immediately, leaving the *calling* thread to be resumed at some
393+
nondeterministic point in the future. Lastly, the [`thread.suspend-then-resume`]
394+
built-in switches execution to the given thread immediately, like
395+
`thread.yield-then-resume`, but leaves the calling thread in the "suspended"
396+
state. These three functions can be used to resume both newly-created threads as
397+
well as threads that executed and then suspended.
398+
399+
Threads can also explicitly put themselves in the "suspended" state without
400+
specifying the other thread to run by calling the [`thread.suspend`] built-in.
401+
This is useful if a thread needs to wait on some condition that will be met by
402+
some unknown thread in the future (which will resume the suspended thread).
403+
Similarly, threads can explicitly put themselves in the "ready to run" state
404+
without specifying the other thread to run by calling [`thread.yield`]. This is
405+
useful if a thread has a long-running computation without I/O but still needs to
406+
allow other cooperative threads to make progress concurrently.
407+
408+
Lastly, in addition to being able to switch to "suspended" threads, threads can
409+
also switch to threads that are in a "ready to run" state by calling the
410+
[`thread.suspend-then-promote`] and [`thread.yield-then-promote`] built-ins
411+
which, like the `thread.{suspend,yield}-then-resume` built-ins, leave the
412+
calling thread in a "suspended" or "ready to run" state, resp. While the calling
413+
thread *may* know that the target thread is ready to run (e.g., because the
414+
target thread yielded or is waiting on a future/stream operation that the
415+
calling thread just completed), in general readiness may depend on
416+
nondeterministic external I/O and the calling thread just wants to yield its
417+
timeslice to the target thread *if* it's ready. Thus, if the target thread
418+
is *not* "ready to run", these built-ins fall back to the behavior of the
419+
untargeted `thread.{suspend,yield}` built-ins.
420+
421+
Together, these thread built-ins support both the "green thread" [use
422+
cases](#goals), where Core WebAssembly code running inside the component wants
423+
to fully control thread scheduling (via the suspending and resuming built-ins)
424+
as well as the "host thread" use cases where the Core WebAssembly code wants to
425+
let the containing runtime nondeterministically schedule threads (via yielding
426+
and promoting built-ins).
427+
417428

418429
### Thread-Local Storage
419430

@@ -475,39 +486,56 @@ traditional synchronous OS syscalls or an asynchronous `io_uring`. However,
475486
when the callee is implemented by another component, the Component Model
476487
defines exactly what counts as "blocking".
477488

478-
At a high level, there are six ways for a call to a component export to block,
479-
all of which are described above or below in more detail:
480-
* calling an `async`-typed function import using the sync ABI
489+
There are several ways for the [task](#threads-and-tasks) created by calling a
490+
component export to potentially block:
481491
* suspending the current thread via the
482-
[`thread.suspend`](#thread-built-ins) built-in
492+
[`thread.suspend{,then-promote}`](#thread-built-ins) built-ins
483493
* cooperatively yielding (e.g., during a long-running computation) via the
484-
[`thread.yield`](#thread-built-ins) built-in
494+
[`thread.yield{,-then-promote}`](#thread-built-ins) built-ins
485495
* waiting for one of a set of concurrent operations to complete via the
486496
[`waitable-set.wait`](#waitables-and-waitable-sets) built-in
487-
* waiting for a stream or future operation to complete via the
497+
* synchronously waiting for a stream or future operation to complete via the
488498
[`{stream,future}.{,cancel-}{read,write}`](#streams-and-futures) built-ins
489-
* waiting for a subtask to cooperatively cancel itself via the
499+
* synchronously waiting for a subtask to cooperatively cancel itself via the
490500
[`subtask.cancel`](#cancellation) built-in
501+
* synchronously calling an `async` function (which may transitively block)
502+
503+
At each of these points, the [current thread](#current-thread-and-task) may
504+
block and be suspended, depending on the operation and the state of the world.
505+
Thus, each of these represents **cooperative yield points**.
506+
507+
If the [current task](#current-thread-and-task) has already returned its value
508+
to the caller, then control flow is transferred back to the caller, which can
509+
now use the return value that was written to memory via the ABI.
510+
511+
However, if the current task has *not* yet returned a value, the behavior
512+
depends on the *function type* of the callee:
513+
514+
If the callee's function type is `async`, then the task is also considered to
515+
"block". According to the async ABI, control flow is then transferred to the
516+
innermost caller using the async ABI (noting that there may be any number of
517+
sync ABI calls on the stack between the innermost async ABI caller and the
518+
blocking callee).
519+
520+
521+
If instead the callee's function type is *not* `async`, then
491522

492-
At each of these points, the [current thread](#current-thread-and-task) will be
523+
"blocked" if there is some other thread in the same component instance that
524+
is in the ["ready to run"](#thread-built-ins) state. In this case, the runtime
525+
switches to another thread (picking nondeterministically if there are multiple
526+
ready threads) and continues doing so until either
527+
528+
529+
However, the
530+
operation
493531
suspended. Execution transfers to a caller's thread if there is one, or
494532
otherwise back to the runtime, which may invoke new component exports or
495-
nondeterministically resume a cooperative thread that is ready to run. Thus,
496-
each of these represents **cooperative yield points**.
497-
498-
Additionally, each of these potentially-blocking operations will trap if the
499-
[current task's function type](#current-thread-and-task) does not declare the
500-
`async` effect, since only `async`-typed functions are allowed to block. As an
501-
exception, to allow it to be called arbitrarily from anywhere, `thread.yield`
502-
does not trap but instead behaves as a no-op if the current task's function
503-
type does not contain `async`.
533+
nondeterministically resume a cooperative thread that is ready to run.
504534

505535
"Blocking" is [specified in terms of] stack-switching, with a `block` effect
506536
that suspends the current thread to produce a continuation that can be resumed
507537
once the reason for blocking is addressed.
508538

509-
The [Canonical ABI explainer] defines the above behavior more precisely; search
510-
for `may_block` to see all the relevant points.
511539

512540
### Waitables and Waitable Sets
513541

@@ -678,12 +706,12 @@ degree of concurrency than synchronous exports. Stackful async exports ignore
678706
the lock entirely and thus achieve the highest degree of (cooperative)
679707
concurrency.
680708

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`.
709+
Since non-`async` functions are not allowed to [block](#blocking) (including due
710+
to backpressure) and also don't pile up like `async` functions, non-`async`
711+
functions ignore backpressure (explicit and implicit) entirely. If a component
712+
exports a mix of `async` and non-`async` functions, code generation must
713+
therefore be prepared to handle non-`async` functions executing at any
714+
cooperative yield point, even in the middle of a `callback`.
687715

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

715743
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.
744+
`task.return` when not in the "started" state traps.
719745

720746
### Borrows
721747

@@ -1538,13 +1564,15 @@ comes after:
15381564
[`waitable-set.wait`]: Explainer.md#-waitable-setwait
15391565
[`waitable-set.poll`]: Explainer.md#-waitable-setpoll
15401566
[`waitable.join`]: Explainer.md#-waitablejoin
1541-
[`thread.new-indirect`]: Explainer.md#-threadnew-indirect
15421567
[`thread.index`]: Explainer.md#-threadindex
1543-
[`thread.suspend`]: Explainer.md#-threadsuspend
1544-
[`thread.switch-to`]: Explainer.md#-threadswitch-to
1568+
[`thread.new-indirect`]: Explainer.md#-threadnew-indirect
15451569
[`thread.resume-later`]: Explainer.md#-threadresume-later
1546-
[`thread.yield-to`]: Explainer.md#-threadyield-to
1570+
[`thread.suspend`]: Explainer.md#-threadsuspend
15471571
[`thread.yield`]: Explainer.md#-threadyield
1572+
[`thread.suspend-then-resume`]: Explainer.md#-threadsuspend-then-resume
1573+
[`thread.yield-then-resume`]: Explainer.md#-threadyield-then-resume
1574+
[`thread.suspend-then-promote`]: Explainer.md#-threadsuspend-then-promote
1575+
[`thread.yield-then-promote`]: Explainer.md#-threadyield-then-promote
15481576
[`{stream,future}.new`]: Explainer.md#-streamnew-and-futurenew
15491577
[`{stream,future}.{read,write}`]: Explainer.md#-streamread-and-streamwrite
15501578
[`stream.cancel-write`]: Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write

design/mvp/Explainer.md

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,8 +1468,10 @@ canon ::= ...
14681468
| (canon thread.resume-later (core func <id>?)) 🧵
14691469
| (canon thread.suspend cancellable? (core func <id>?)) 🧵
14701470
| (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>?)) 🧵
1471+
| (canon thread.suspend-then-resume cancellable? (core func <id>?)) 🧵
1472+
| (canon thread.yield-then-resume cancellable? (core func <id>?)) 🧵
1473+
| (canon thread.suspend-then-promote cancellable? (core func <id>?)) 🧵
1474+
| (canon thread.yield-then-promote cancellable? (core func <id>?)) 🧵
14731475
| (canon error-context.new <canonopt>* (core func <id>?)) 📝
14741476
| (canon error-context.debug-message <canonopt>* (core func <id>?)) 📝
14751477
| (canon error-context.drop (core func <id>?)) 📝
@@ -2089,7 +2091,7 @@ extended for [GC].
20892091

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

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

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-
21282127
For details, see [Thread Built-ins] in the concurrency explainer and
21292128
[`canon_thread_suspend`] in the Canonical ABI explainer.
21302129

@@ -2138,52 +2137,73 @@ For details, see [Thread Built-ins] in the concurrency explainer and
21382137
The `thread.yield` built-in allows the runtime to potentially switch to any
21392138
other thread in the "ready" state, enabling a long-running computation to
21402139
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.
2140+
thread to be resumed (as with `thread.yield-then-resume`). If `cancellable` is
2141+
set, `thread.yield` returns whether the current task was [cancelled] by the
2142+
caller; otherwise, `thread.yield` always returns `false`.
21512143

21522144
For details, see [Thread Built-ins] in the concurrency explainer and
21532145
[`canon_thread_yield`] in the Canonical ABI explainer.
21542146

2155-
###### 🧵 `thread.switch-to`
2147+
###### 🧵 `thread.suspend-then-resume`
2148+
2149+
| Synopsis | |
2150+
| -------------------------- | --------------------------------------- |
2151+
| Approximate WIT signature | `func<cancellable?>(t: thread) -> bool` |
2152+
| Canonical ABI signature | `[t:i32] -> [i32]` |
2153+
2154+
The `thread.suspend-then-resume` built-in suspends the [current thread] and
2155+
immediately resumes execution of the thread `t`, trapping if `t` is not in a
2156+
"suspended" state. If `cancellable` is set, `thread.suspend-then-resume` returns
2157+
whether the current task was [cancelled] by the caller; otherwise,
2158+
`thread.suspend-then-resume` always returns `false`.
2159+
2160+
For details, see [Thread Built-ins] in the concurrency explainer and
2161+
[`canon_thread_suspend_then_resume`] in the Canonical ABI explainer.
2162+
2163+
###### 🧵 `thread.yield-then-resume`
21562164

21572165
| Synopsis | |
21582166
| -------------------------- | --------------------------------------- |
21592167
| Approximate WIT signature | `func<cancellable?>(t: thread) -> bool` |
21602168
| Canonical ABI signature | `[t:i32] -> [i32]` |
21612169

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`.
2170+
The `thread.yield-then-resume` built-in immediately resumes execution of the
2171+
thread `t`, (trapping if `t` is not in a "suspended" state) leaving the [current
2172+
thread] in a "ready" state so that the runtime can nondeterministically resume
2173+
the current thread at some point in the future. If `cancellable` is set,
2174+
`thread.yield-then-resume` returns whether the current task was [cancelled] by
2175+
the caller; otherwise, `thread.yield-then-resume` always returns `false`.
21672176

21682177
For details, see [Thread Built-ins] in the concurrency explainer and
2169-
[`canon_thread_switch_to`] in the Canonical ABI explainer.
2178+
[`canon_thread_yield_then_resume`] in the Canonical ABI explainer.
21702179

2171-
###### 🧵 `thread.yield-to`
2180+
###### 🧵 `thread.suspend-then-promote`
21722181

21732182
| Synopsis | |
21742183
| -------------------------- | --------------------------------------- |
21752184
| Approximate WIT signature | `func<cancellable?>(t: thread) -> bool` |
21762185
| Canonical ABI signature | `[t:i32] -> [i32]` |
21772186

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`.
2187+
The `thread.suspend-then-promote` built-in immediately resumes execution of the
2188+
thread `t` if `t` is in a "ready" state, in any case leaving the current thread
2189+
in a "suspended" state.
21842190

21852191
For details, see [Thread Built-ins] in the concurrency explainer and
2186-
[`canon_thread_yield_to`] in the Canonical ABI explainer.
2192+
[`canon_thread_suspend_then_promote`] in the Canonical ABI explainer.
2193+
2194+
###### 🧵 `thread.yield-then-promote`
2195+
2196+
| Synopsis | |
2197+
| -------------------------- | --------------------------------------- |
2198+
| Approximate WIT signature | `func<cancellable?>(t: thread) -> bool` |
2199+
| Canonical ABI signature | `[t:i32] -> [i32]` |
2200+
2201+
The `thread.yield-then-promote` built-in immediately resumes execution of the
2202+
thread `t` if `t` is in a "ready" state, in any case leaving the current thread
2203+
in a "ready" state.
2204+
2205+
For details, see [Thread Built-ins] in the concurrency explainer and
2206+
[`canon_thread_yield_then_promote`] in the Canonical ABI explainer.
21872207

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

@@ -3283,11 +3303,13 @@ For some use-case-focused, worked examples, see:
32833303
[`canon_error_context_drop`]: CanonicalABI.md#-canon-error-contextdrop
32843304
[`canon_thread_index`]: CanonicalABI.md#-canon-threadindex
32853305
[`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
32883306
[`canon_thread_resume_later`]: CanonicalABI.md#-canon-threadresume-later
3289-
[`canon_thread_yield_to`]: CanonicalABI.md#-canon-threadyield-to
3307+
[`canon_thread_suspend`]: CanonicalABI.md#-canon-threadsuspend
32903308
[`canon_thread_yield`]: CanonicalABI.md#-canon-threadyield
3309+
[`canon_thread_suspend_then_resume`]: CanonicalABI.md#-canon-threadsuspend-then-resume
3310+
[`canon_thread_yield_then_resume`]: CanonicalABI.md#-canon-threadyield-then-resume
3311+
[`canon_thread_suspend_then_promote`]: CanonicalABI.md#-canon-threadsuspend-then-promote
3312+
[`canon_thread_yield_then_promote`]: CanonicalABI.md#-canon-threadyield-then-promote
32913313
[`canon_thread_spawn_ref`]: CanonicalABI.md#-canon-threadspawn-ref
32923314
[`canon_thread_spawn_indirect`]: CanonicalABI.md#-canon-threadspawn-indirect
32933315
[`canon_thread_available_parallelism`]: CanonicalABI.md#-canon-threadavailable_parallelism

0 commit comments

Comments
 (0)