Skip to content

Commit 78a5aa6

Browse files
committed
Add thread.{get,set}-task and task.drop built-ins
Resolves #659
1 parent 0de24ba commit 78a5aa6

10 files changed

Lines changed: 2033 additions & 151 deletions

File tree

‎design/mvp/Binary.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,9 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
342342
| 0x2b 0x00 => (canon thread.yield-then-resume (core func)) 🧵
343343
| 0x2c 0x00 => (canon thread.suspend-then-promote (core func)) 🧵
344344
| 0x2d 0x00 => (canon thread.yield-then-promote (core func)) 🧵
345+
| 0x30 => (canon thread.set-task (core func)) 🧵
346+
| 0x31 => (canon thread.get-task (core func)) 🧵
347+
| 0x32 => (canon task.drop (core func)) 🧵
345348
| 0x40 shared?:<sh?> ft:<core:typeidx> => (canon thread.spawn-ref shared? ft (core func)) 🧵②
346349
| 0x41 shared?:<sh?> ft:<core:typeidx> tbl:<core:tableidx> => (canon thread.spawn-indirect shared? ft tbl (core func)) 🧵②
347350
| 0x42 shared?:<sh?> => (canon thread.available-parallelism shared? (core func)) 🧵②

‎design/mvp/CanonicalABI.md‎

Lines changed: 144 additions & 83 deletions
Large diffs are not rendered by default.

‎design/mvp/Concurrency.md‎

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,16 @@ Thread
268268
where a **component store** is the top-level "thing" and analogous to a Core
269269
WebAssembly [store].
270270

271-
The reason for the thread/task split is that, when one thread creates a new
272-
thread, the new thread is contained by the task of the original thread which
273-
creates an N:1 relationship between threads and tasks that ties N threads to
274-
the original export call (= "task") that transitively spawned those N threads.
275-
This relationship serves several purposes described in the following sections.
271+
When a component export is called, one new task is created for the call; this
272+
task contains one new *implicit* thread that executes it. The reason for the
273+
thread/task split is that this implicit thread may then go on to spawn N more
274+
*explicit* threads (via `thread.new-indirect`) that are initially contained by
275+
the same task, thereby creating an N:1 relationship between threads and tasks.
276+
277+
While the store:instance and instance:task relationships are immutably set on
278+
creation, the task:thread relationship is *mutable*: guest code running inside a
279+
component instance can change which task a thread is currently executing on
280+
behalf of by calling the [`thread.set-task`] built-in, as described below.
276281

277282
In the Canonical ABI explainer, threads, tasks, component instances and
278283
component stores are represented by the [`Thread`], [`Task`],
@@ -311,9 +316,15 @@ supertask, they can be thought of as a single node in the async call stack.
311316

312317
A subtask/supertask relationship is immutably established when an import is
313318
called, setting the [current task](#current-thread-and-task) as the supertask
314-
of the new subtask created for the import call. Thus, one reason for
315-
associating every thread with a "containing task" is to ensure that there is
316-
always a well-defined async call stack.
319+
of the new subtask created for the import call. Thus, one reason for associating
320+
every thread with a "containing task" is to ensure that there is always a
321+
well-defined async call stack. Note that guest code can call [`thread.set-task`]
322+
to change the containing task of a thread and thus the async call stack can
323+
change completely between two program points while executing a single thread.
324+
For example, when a JS runtime flushes its microtask queue and encounters a JS
325+
callback associated with a task other than the current thread's task, the JS
326+
runtime would call `thread.set-task` so that the async call stack matches the JS
327+
developer's expectation.
317328

318329
The async call stack is not currently observable to running components, except
319330
that it may nondeterministically appear as part of the callstack stored in
@@ -336,9 +347,9 @@ not enforcing a stricter form of Structured Concurrency at the Component Model
336347
level is that there are important use cases where forcing a supertask's thread
337348
to stay resident just to wait for subtasks to finish would waste resources
338349
without tangible benefit. Instead, we can say that once a supertask's last
339-
thread finishes execution, the supertask semantically "tail calls" any still-
340-
executing subtasks, staying technically-alive and on the async call stack until
341-
they complete, but not consuming real resources.
350+
thread exits or switches to another task, the supertask semantically "tail
351+
calls" any still-executing subtasks, staying technically-alive and on the
352+
async call stack until they complete, but not consuming real resources.
342353

343354
For scenarios where one component wants to *non-cooperatively* put an upper
344355
bound on execution of a call into another component, a separate "[blast zone]"
@@ -374,13 +385,23 @@ New threads are created with the [`thread.new-indirect`] built-in. As mentioned
374385
[above](#threads-and-tasks), a spawned thread inherits the task of the spawning
375386
thread which is why threads and tasks are N:1. `thread.new-indirect` adds a new
376387
thread to the component instance's threads table and returns the `i32` index of
377-
this table entry to the Core WebAssembly caller. Like [`pthread_create`],
378-
`thread.new-indirect` takes a Core WebAssembly function (via index into a
379-
`funcref` table) and a "closure" parameter to pass to the function when called
380-
on the new thread. However, unlike `pthread_create`, the new thread is
381-
initially in a "suspended" state and must be explicitly "resumed" using one of
382-
the following 3 thread built-ins. Once the thread is resumed, the thread can
383-
learn its own index by calling the [`thread.index`] built-in.
388+
this table entry to the Core WebAssembly caller.
389+
390+
After creation, the implicitly-set containing task of a thread can be explicitly
391+
overridden using the [`thread.set-task`] built-in. `thread.set-task` sets the
392+
containing task of the current thread to a task handle that was retrieved via
393+
[`thread.get-task`]. `thread.get-task` always allocates a fresh handle storing a
394+
reference to the current thread's containing task. The `i32` index of this new
395+
handle must later be explicitly dropped via [`task.drop`] to avoid leaking
396+
the task. Tasks are thus kept alive by any or all of: contained threads, subtask
397+
handles and task handles.
398+
399+
Like [`pthread_create`], `thread.new-indirect` takes a Core WebAssembly function
400+
(via index into a `funcref` table) and a "closure" parameter to pass to the
401+
function when called on the new thread. However, unlike `pthread_create`, the
402+
new thread is initially in a "suspended" state and must be explicitly "resumed"
403+
using one of the following 3 thread built-ins. Once the thread is resumed, the
404+
thread can learn its own index by calling the [`thread.index`] built-in.
384405

385406
A suspended thread (identified by thread-table index) can be resumed at some
386407
nondeterministic point in future via the [`thread.resume-later`] built-in. In
@@ -733,10 +754,7 @@ the "started" state.
733754
The way an `async` export returns its value using the async ABI is by calling
734755
[`task.return`], passing the core values that are to be lifted as *parameters*.
735756
When using the async ABI, *any* of the threads contained by a task can call
736-
`task.return`; there is no "main thread" of a task. When the last thread of a
737-
task returns, there is a trap if `task.return` has not been called. Thus, *some*
738-
thread (either the thread created implicitly for the initial export call or some
739-
thread transitively created by that thread) must call `task.return`.
757+
`task.return`; there is no "main thread" of a task.
740758

741759
Returning values by calling `task.return` allows a task to continue executing
742760
even after it has passed its initial results to the caller. This is also
@@ -752,7 +770,7 @@ the readable end passed for `in`) and `stream.write`s (of the writable end it
752770
`stream.new`ed) before exiting the task.
753771

754772
Once `task.return` is called, the task is in the "returned" state. Calling
755-
`task.return` when not in the "started" state traps.
773+
`task.return` when already in a resolved state traps.
756774

757775
### Borrows
758776

@@ -1564,6 +1582,9 @@ the concurrency story:
15641582
[`thread.yield-then-resume`]: Explainer.md#-threadyield-then-resume
15651583
[`thread.suspend-then-promote`]: Explainer.md#-threadsuspend-then-promote
15661584
[`thread.yield-then-promote`]: Explainer.md#-threadyield-then-promote
1585+
[`thread.set-task`]: Explainer.md#-threadset-task
1586+
[`thread.get-task`]: Explainer.md#-threadget-task
1587+
[`task.drop`]: Explainer.md#-taskdrop
15671588
[`{stream,future}.new`]: Explainer.md#-streamnew-and-futurenew
15681589
[`{stream,future}.{read,write}`]: Explainer.md#-streamread-and-streamwrite
15691590
[`stream.cancel-write`]: Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write

‎design/mvp/Explainer.md‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,9 @@ canon ::= ...
15931593
| (canon thread.yield-then-resume (core func <id>?)) 🧵
15941594
| (canon thread.suspend-then-promote (core func <id>?)) 🧵
15951595
| (canon thread.yield-then-promote (core func <id>?)) 🧵
1596+
| (canon thread.set-task (core func <id>?)) 🧵
1597+
| (canon thread.get-task (core func <id>?)) 🧵
1598+
| (canon task.drop (core func <id>?)) 🧵
15961599
| (canon error-context.new <canonopt>* (core func <id>?)) 📝
15971600
| (canon error-context.debug-message <canonopt>* (core func <id>?)) 📝
15981601
| (canon error-context.drop (core func <id>?)) 📝
@@ -1748,7 +1751,7 @@ For details, see [Backpressure] in the concurrency explainer and
17481751
| Canonical ABI signature | `[lower(FuncT.results)*] -> []` |
17491752

17501753
The `task.return` built-in takes as parameters the result values of the
1751-
[current task]. One of `task.return` or `task.cancel` must be called exactly
1754+
[current task]. One of `task.return` or `task.cancel` must be called at most
17521755
once from any of a task's threads.
17531756

17541757
The `canon task.return` definition takes component-level return type and the
@@ -2320,6 +2323,60 @@ returned `i32` is always `0` and may be removed in a future ABI revision.
23202323
For details, see [Thread Built-ins] in the concurrency explainer and
23212324
[`canon_thread_yield_then_promote`] in the Canonical ABI explainer.
23222325

2326+
###### 🧵 `thread.set-task`
2327+
2328+
| Synopsis | |
2329+
| -------------------------- | --------------- |
2330+
| Approximate WIT signature | `func(t: task)` |
2331+
| Canonical ABI signature | `[t:i32] -> []` |
2332+
2333+
The `thread.set-task` built-in allows the [current thread] to set its containing
2334+
task to the given operand, which immediately changes the [current task].
2335+
Built-ins like `task.return` and `task.cancel` are defined in terms of "the
2336+
current task", and thus `thread.set-task` allows a thread to return a value for
2337+
a task other than the task that initially spawned the thread.
2338+
2339+
Tasks form an [async call stack] that is consulted for debugging, observability,
2340+
and host import-to-export call attribution. Thus, changing the current task
2341+
allows a guest's concurrency runtime to control what async call stack to
2342+
associate with the current wasm execution.
2343+
2344+
The `i32` index passed to `thread.set-task` must be a task handle which can
2345+
currently only be retrieved by calling `thread.get-task`.
2346+
2347+
For details, see [Thread Built-ins] in the concurrency explainer and
2348+
[`canon_thread_set_task`] in the Canonical ABI explainer.
2349+
2350+
###### 🧵 `thread.get-task`
2351+
2352+
| Synopsis | |
2353+
| -------------------------- | ---------------- |
2354+
| Approximate WIT signature | `func() -> task` |
2355+
| Canonical ABI signature | `[] -> [i32]` |
2356+
2357+
The `thread.get-task` built-in returns a handle referring to the [current task]
2358+
(which is the containing task of the [current thread]). This handle is currently
2359+
only used as an argument in `thread.set-task`. Each call to `thread.get-task`
2360+
returns a fresh handle which must be released by `task.drop` to avoid a handle
2361+
table leak.
2362+
2363+
For details, see [Thread Built-ins] in the concurrency explainer and
2364+
[`canon_thread_get_task`] in the Canonical ABI explainer.
2365+
2366+
###### 🧵 `task.drop`
2367+
2368+
| Synopsis | |
2369+
| -------------------------- | --------------- |
2370+
| Approximate WIT signature | `func(t: task)` |
2371+
| Canonical ABI signature | `[t:i32] -> []` |
2372+
2373+
The `task.drop` built-in drops a handle allocated by `thread.get-task`. The
2374+
state of the underlying task is not modified nor is the task destroyed, as it
2375+
may have other active referents.
2376+
2377+
For details, see [Thread Built-ins] in the concurrency explainer and
2378+
[`canon_task_drop`] in the Canonical ABI explainer.
2379+
23232380
###### 🧵② `thread.spawn-ref`
23242381

23252382
| Synopsis | |
@@ -3474,6 +3531,9 @@ For some use-case-focused, worked examples, see:
34743531
[`canon_thread_yield_then_resume`]: CanonicalABI.md#-canon-threadyield-then-resume
34753532
[`canon_thread_suspend_then_promote`]: CanonicalABI.md#-canon-threadsuspend-then-promote
34763533
[`canon_thread_yield_then_promote`]: CanonicalABI.md#-canon-threadyield-then-promote
3534+
[`canon_thread_get_task`]: CanonicalABI.md#-canon-threadget-task
3535+
[`canon_thread_set_task`]: CanonicalABI.md#-canon-threadset-task
3536+
[`canon_task_drop`]: CanonicalABI.md#-canon-taskdrop
34773537
[`canon_thread_spawn_ref`]: CanonicalABI.md#-canon-threadspawn-ref
34783538
[`canon_thread_spawn_indirect`]: CanonicalABI.md#-canon-threadspawn-indirect
34793539
[`canon_thread_available_parallelism`]: CanonicalABI.md#-canon-threadavailable_parallelism
@@ -3483,6 +3543,7 @@ For some use-case-focused, worked examples, see:
34833543
[Summary]: Concurrency.md#summary
34843544
[Current Thread]: Concurrency.md#current-thread-and-task
34853545
[Current Task]: Concurrency.md#current-thread-and-task
3546+
[Async Call Stack]: Concurrency.md#subtasks-and-supertasks
34863547
[Thread-Local Storage]: Concurrency.md#thread-local-storage
34873548
[Subtask]: Concurrency.md#subtasks-and-supertasks
34883549
[Stream or Future]: Concurrency.md#streams-and-futures

0 commit comments

Comments
 (0)