Skip to content

Commit 71e494f

Browse files
committed
Add thread.{get,set}-task and task.drop built-ins
1 parent 1ec5545 commit 71e494f

10 files changed

Lines changed: 1084 additions & 34 deletions

File tree

design/mvp/Binary.md

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

design/mvp/CanonicalABI.md

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ specified here.
6767
* [`canon thread.yield-then-resume`](#-canon-threadyield-then-resume) 🧵
6868
* [`canon thread.suspend-then-promote`](#-canon-threadsuspend-then-promote) 🧵
6969
* [`canon thread.yield-then-promote`](#-canon-threadyield-then-promote) 🧵
70+
* [`canon thread.get-task`](#-canon-threadget-task) 🧵
71+
* [`canon thread.set-task`](#-canon-threadset-task) 🧵
72+
* [`canon task.drop`](#-canon-taskdrop) 🧵
7073
* [`canon error-context.new`](#-canon-error-contextnew) 📝
7174
* [`canon error-context.debug-message`](#-canon-error-contextdebug-message) 📝
7275
* [`canon error-context.drop`](#-canon-error-contextdrop) 📝
@@ -121,7 +124,7 @@ into the `handles` or `threads` fields of `ComponentInstance`.
121124
```python
122125
class ComponentInstance:
123126
store: Store
124-
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
127+
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext | Task]
125128
threads: Table[Thread]
126129
may_leave: bool
127130
backpressure: int
@@ -3425,10 +3428,12 @@ are lifted according to `lift_flat_values` above, the optional `post-return`
34253428
function (specified as a `canonopt` immediate of `canon lift`) is called,
34263429
passing the same core wasm results as parameters so that the `post-return`
34273430
function can free any associated allocations.
3431+
TODO: copy from set-task-old (and also alll the other stuff in Concurrency.md)
34283432
```python
34293433
if not opts.async_:
34303434
flat_results = call_and_trap_on_throw(callee, flat_args)
34313435
assert(types_match_values(flat_ft.results, flat_results))
3436+
trap_if(thread.task is not task)
34323437
result = lift_flat_values(cx, MAX_FLAT_RESULTS, CoreValueIter(flat_results), ft.result_type())
34333438
task.return_(result)
34343439
if opts.post_return is not None:
@@ -4673,17 +4678,17 @@ class CoreFuncRef:
46734678
callee: Callable[[list[CoreValType]], list[CoreValType]]
46744679

46754680
def canon_thread_new_indirect(ft, ftbl: Table[CoreFuncRef], fi, c):
4676-
task = current_task()
4677-
trap_if(not task.inst.may_leave)
4681+
inst = current_instance()
4682+
trap_if(not inst.may_leave)
46784683
f = ftbl.get(fi)
46794684
assert(ft == CoreFuncType(['i32'], []) or ft == CoreFuncType(['i64'], []))
46804685
trap_if(f.t != ft)
46814686
def thread_func():
46824687
[] = call_and_trap_on_throw(f.callee, [c])
4683-
task.inst.threads.remove(new_thread.index)
4684-
new_thread = Thread(task, thread_func)
4688+
inst.threads.remove(new_thread.index)
4689+
new_thread = Thread(current_task(), thread_func)
46854690
assert(new_thread.suspended())
4686-
new_thread.index = task.inst.threads.add(new_thread)
4691+
new_thread.index = inst.threads.add(new_thread)
46874692
return [new_thread.index]
46884693
```
46894694
The newly-created thread starts out in a "suspended" state and so, to
@@ -4862,6 +4867,66 @@ def canon_thread_yield_then_promote(i):
48624867
```
48634868

48644869

4870+
###### 🧵 `canon thread.get-task`
4871+
4872+
For a canonical definition:
4873+
```wat
4874+
(canon thread.get-task (core func $thread.get-task))
4875+
```
4876+
validation specifies:
4877+
* `$thread.get-task` is given type `(func (result i32))`
4878+
4879+
Calling `$thread.get-task` invokes the following function which TODO
4880+
```python
4881+
def canon_thread_get_task():
4882+
thread = current_thread()
4883+
trap_if(not thread.task.inst.may_leave)
4884+
taski = thread.task.inst.handles.add(thread.task)
4885+
return [taski]
4886+
```
4887+
4888+
4889+
###### 🧵 `canon thread.set-task`
4890+
4891+
For a canonical definition:
4892+
```wat
4893+
(canon thread.set-task (core func $thread.set-task))
4894+
```
4895+
validation specifies:
4896+
* `$thread.set-task` is given type `(func (param i32))`
4897+
4898+
Calling `$thread.set-task` invokes the following function which TODO
4899+
```python
4900+
def canon_thread_set_task(taski):
4901+
thread = current_thread()
4902+
trap_if(not thread.task.inst.may_leave)
4903+
new_task = thread.task.inst.handles.get(taski)
4904+
trap_if(not isinstance(new_task, Task))
4905+
thread.task = new_task
4906+
return []
4907+
```
4908+
4909+
4910+
###### 🧵 `canon task.drop`
4911+
4912+
For a canonical definition:
4913+
```wat
4914+
(canon thread.drop (core func $task.drop))
4915+
```
4916+
validation specifies:
4917+
* `$thread.drop` is given type `(func (param i32))`
4918+
4919+
Calling `$task.drop` invokes the following function which TODO
4920+
```python
4921+
def canon_task_drop(taski):
4922+
inst = current_instance()
4923+
trap_if(not inst.may_leave)
4924+
task = inst.handles.remove(taski)
4925+
trap_if(not isinstance(task, Task))
4926+
return []
4927+
```
4928+
4929+
48654930
### 📝 `canon error-context.new`
48664931

48674932
For a canonical definition:

design/mvp/Concurrency.md

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,14 @@ 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+
The reason for the thread/task split is that a single component export call,
272+
which spawns a task, can spawn N more threads as part of the execution of that
273+
task, thereby creating an N:1 relationship between threads and tasks.
274+
275+
While the store:instance and instance:task relationships are immutably set on
276+
creation, the task:thread relationship is *mutable*: guest code running inside a
277+
component instance can change which task a thread is currently executing on
278+
behalf of by calling the [`thread.set-task`] built-in, as described below.
276279

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

312315
A subtask/supertask relationship is immutably established when an import is
313316
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.
317+
of the new subtask created for the import call. Thus, one reason for associating
318+
every thread with a "containing task" is to ensure that there is always a
319+
well-defined async call stack. Note that guest code can call [`thread.set-task`]
320+
to change the containing task of a thread and thus the async call stack can
321+
change completely between two program points while executing on a single thread.
322+
For example, when a JS runtime flushes its microtask queue and encounters a
323+
JS callback associated with a task other than the current thread's task, the JS
324+
runtime would call `thread.set-task` so that the async call stack matches the
325+
JS developer's expectation.
317326

318327
The async call stack is not currently observable to running components, except
319328
that it may nondeterministically appear as part of the callstack stored in
@@ -336,7 +345,7 @@ not enforcing a stricter form of Structured Concurrency at the Component Model
336345
level is that there are important use cases where forcing a supertask's thread
337346
to stay resident just to wait for subtasks to finish would waste resources
338347
without tangible benefit. Instead, we can say that once a supertask's last
339-
thread finishes execution, the supertask semantically "tail calls" any still-
348+
thread exits or leaves, the supertask semantically "tail calls" any still-
340349
executing subtasks, staying technically-alive and on the async call stack until
341350
they complete, but not consuming real resources.
342351

@@ -374,13 +383,23 @@ New threads are created with the [`thread.new-indirect`] built-in. As mentioned
374383
[above](#threads-and-tasks), a spawned thread inherits the task of the spawning
375384
thread which is why threads and tasks are N:1. `thread.new-indirect` adds a new
376385
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.
386+
this table entry to the Core WebAssembly caller.
387+
388+
After creation, the implicitly-set containing task of a thread can be explicitly
389+
overridden using the [`thread.set-task`] built-in. `thread.set-task` sets the
390+
containing task of the current thread to the task of some other thread that was
391+
retrieved via [`thread.get-task`]. `thread.get-task` always allocates a fresh
392+
handle storing a reference to the current thread's containing task, returning
393+
the `i32` of this new handle which must be explicitly dropped via [`task.drop`]
394+
to avoid leaking the task. Tasks are thus (acyclicly) kept alive by any or all
395+
of: contained threads, subtask handles and task handles.
396+
397+
Like [`pthread_create`], `thread.new-indirect` takes a Core WebAssembly function
398+
(via index into a `funcref` table) and a "closure" parameter to pass to the
399+
function when called on the new thread. However, unlike `pthread_create`, the
400+
new thread is initially in a "suspended" state and must be explicitly "resumed"
401+
using one of the following 3 thread built-ins. Once the thread is resumed, the
402+
thread can learn its own index by calling the [`thread.index`] built-in.
384403

385404
A suspended thread (identified by thread-table index) can be resumed at some
386405
nondeterministic point in future via the [`thread.resume-later`] built-in. In
@@ -725,10 +744,7 @@ the "started" state.
725744
The way an `async` export returns its value using the async ABI is by calling
726745
[`task.return`], passing the core values that are to be lifted as *parameters*.
727746
When using the async ABI, *any* of the threads contained by a task can call
728-
`task.return`; there is no "main thread" of a task. When the last thread of a
729-
task returns, there is a trap if `task.return` has not been called. Thus, *some*
730-
thread (either the thread created implicitly for the initial export call or some
731-
thread transitively created by that thread) must call `task.return`.
747+
`task.return`; there is no "main thread" of a task.
732748

733749
Returning values by calling `task.return` allows a task to continue executing
734750
even after it has passed its initial results to the caller. This is also
@@ -1560,6 +1576,9 @@ the concurrency story:
15601576
[`thread.yield-then-resume`]: Explainer.md#-threadyield-then-resume
15611577
[`thread.suspend-then-promote`]: Explainer.md#-threadsuspend-then-promote
15621578
[`thread.yield-then-promote`]: Explainer.md#-threadyield-then-promote
1579+
[`thread.set-task`]: Explainer.md#-threadset-task
1580+
[`thread.get-task`]: Explainer.md#-threadget-task
1581+
[`task.drop`]: Explainer.md#-taskdrop
15631582
[`{stream,future}.new`]: Explainer.md#-streamnew-and-futurenew
15641583
[`{stream,future}.{read,write}`]: Explainer.md#-streamread-and-streamwrite
15651584
[`stream.cancel-write`]: Explainer.md#-streamcancel-read-streamcancel-write-futurecancel-read-and-futurecancel-write

design/mvp/Explainer.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,6 +1590,9 @@ canon ::= ...
15901590
| (canon thread.yield-then-resume (core func <id>?)) 🧵
15911591
| (canon thread.suspend-then-promote (core func <id>?)) 🧵
15921592
| (canon thread.yield-then-promote (core func <id>?)) 🧵
1593+
| (canon thread.get-task (core func <id>?)) 🧵
1594+
| (canon thread.set-task (core func <id>?)) 🧵
1595+
| (canon task.drop (core func <id>?)) 🧵
15931596
| (canon error-context.new <canonopt>* (core func <id>?)) 📝
15941597
| (canon error-context.debug-message <canonopt>* (core func <id>?)) 📝
15951598
| (canon error-context.drop (core func <id>?)) 📝
@@ -2298,6 +2301,43 @@ returned `i32` is always `0` and may be removed in a future ABI revision.
22982301
For details, see [Thread Built-ins] in the concurrency explainer and
22992302
[`canon_thread_yield_then_promote`] in the Canonical ABI explainer.
23002303

2304+
###### 🧵 `thread.get-task`
2305+
2306+
| Synopsis | |
2307+
| -------------------------- | ---------------- |
2308+
| Approximate WIT signature | `func() -> task` |
2309+
| Canonical ABI signature | `[] -> [i32]` |
2310+
2311+
TODO
2312+
2313+
For details, see [Thread Built-ins] in the concurrency explainer and
2314+
[`canon_thread_get_task`] in the Canonical ABI explainer.
2315+
2316+
###### 🧵 `thread.set-task`
2317+
2318+
| Synopsis | |
2319+
| -------------------------- | --------------- |
2320+
| Approximate WIT signature | `func(t: task)` |
2321+
| Canonical ABI signature | `[t:i32] -> []` |
2322+
2323+
TODO
2324+
2325+
For details, see [Thread Built-ins] in the concurrency explainer and
2326+
[`canon_thread_set_task`] in the Canonical ABI explainer.
2327+
2328+
###### 🧵 `task.drop`
2329+
2330+
| Synopsis | |
2331+
| -------------------------- | --------------- |
2332+
| Approximate WIT signature | `func(t: task)` |
2333+
| Canonical ABI signature | `[t:i32] -> []` |
2334+
2335+
TODO
2336+
2337+
For details, see [Thread Built-ins] in the concurrency explainer and
2338+
[`canon_task_drop`] in the Canonical ABI explainer.
2339+
2340+
23012341
###### 🧵② `thread.spawn-ref`
23022342

23032343
| Synopsis | |
@@ -3440,6 +3480,9 @@ For some use-case-focused, worked examples, see:
34403480
[`canon_thread_yield_then_resume`]: CanonicalABI.md#-canon-threadyield-then-resume
34413481
[`canon_thread_suspend_then_promote`]: CanonicalABI.md#-canon-threadsuspend-then-promote
34423482
[`canon_thread_yield_then_promote`]: CanonicalABI.md#-canon-threadyield-then-promote
3483+
[`canon_thread_get_task`]: CanonicalABI.md#-canon-threadget-task
3484+
[`canon_thread_set_task`]: CanonicalABI.md#-canon-threadset-task
3485+
[`canon_task_drop`]: CanonicalABI.md#-canon-taskdrop
34433486
[`canon_thread_spawn_ref`]: CanonicalABI.md#-canon-threadspawn-ref
34443487
[`canon_thread_spawn_indirect`]: CanonicalABI.md#-canon-threadspawn-indirect
34453488
[`canon_thread_available_parallelism`]: CanonicalABI.md#-canon-threadavailable_parallelism

design/mvp/canonical-abi/definitions.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ class FutureType(ValType):
190190

191191
class ComponentInstance:
192192
store: Store
193-
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
193+
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext | Task]
194194
threads: Table[Thread]
195195
may_leave: bool
196196
backpressure: int
@@ -2090,6 +2090,7 @@ def thread_func():
20902090
if not opts.async_:
20912091
flat_results = call_and_trap_on_throw(callee, flat_args)
20922092
assert(types_match_values(flat_ft.results, flat_results))
2093+
trap_if(thread.task is not task)
20932094
result = lift_flat_values(cx, MAX_FLAT_RESULTS, CoreValueIter(flat_results), ft.result_type())
20942095
task.return_(result)
20952096
if opts.post_return is not None:
@@ -2622,17 +2623,17 @@ class CoreFuncRef:
26222623
callee: Callable[[list[CoreValType]], list[CoreValType]]
26232624

26242625
def canon_thread_new_indirect(ft, ftbl: Table[CoreFuncRef], fi, c):
2625-
task = current_task()
2626-
trap_if(not task.inst.may_leave)
2626+
inst = current_instance()
2627+
trap_if(not inst.may_leave)
26272628
f = ftbl.get(fi)
26282629
assert(ft == CoreFuncType(['i32'], []) or ft == CoreFuncType(['i64'], []))
26292630
trap_if(f.t != ft)
26302631
def thread_func():
26312632
[] = call_and_trap_on_throw(f.callee, [c])
2632-
task.inst.threads.remove(new_thread.index)
2633-
new_thread = Thread(task, thread_func)
2633+
inst.threads.remove(new_thread.index)
2634+
new_thread = Thread(current_task(), thread_func)
26342635
assert(new_thread.suspended())
2635-
new_thread.index = task.inst.threads.add(new_thread)
2636+
new_thread.index = inst.threads.add(new_thread)
26362637
return [new_thread.index]
26372638

26382639
### 🧵 `canon thread.resume-later`
@@ -2704,6 +2705,33 @@ def canon_thread_yield_then_promote(i):
27042705
thread.yield_then_promote(other_thread)
27052706
return [0]
27062707

2708+
### 🧵 `canon thread.get-task`
2709+
2710+
def canon_thread_get_task():
2711+
thread = current_thread()
2712+
trap_if(not thread.task.inst.may_leave)
2713+
taski = thread.task.inst.handles.add(thread.task)
2714+
return [taski]
2715+
2716+
### 🧵 `canon thread.set-task`
2717+
2718+
def canon_thread_set_task(taski):
2719+
thread = current_thread()
2720+
trap_if(not thread.task.inst.may_leave)
2721+
new_task = thread.task.inst.handles.get(taski)
2722+
trap_if(not isinstance(new_task, Task))
2723+
thread.task = new_task
2724+
return []
2725+
2726+
### 🧵 `canon task.drop`
2727+
2728+
def canon_task_drop(taski):
2729+
inst = current_instance()
2730+
trap_if(not inst.may_leave)
2731+
task = inst.handles.remove(taski)
2732+
trap_if(not isinstance(task, Task))
2733+
return []
2734+
27072735
### 📝 `canon error-context.new`
27082736

27092737
@dataclass

0 commit comments

Comments
 (0)