Skip to content

Commit 9cf3d89

Browse files
committed
Add thread.{get,set}-task and task.drop built-ins
1 parent c52323b commit 9cf3d89

10 files changed

Lines changed: 1132 additions & 128 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: 104 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ specified here.
6565
* [`canon thread.yield-then-resume`](#-canon-threadyield-then-resume) 🧵
6666
* [`canon thread.suspend-then-promote`](#-canon-threadsuspend-then-promote) 🧵
6767
* [`canon thread.yield-then-promote`](#-canon-threadyield-then-promote) 🧵
68+
* [`canon thread.get-task`](#-canon-threadget-task) 🧵
69+
* [`canon thread.set-task`](#-canon-threadset-task) 🧵
70+
* [`canon task.drop`](#-canon-taskdrop) 🧵
6871
* [`canon error-context.new`](#-canon-error-contextnew) 📝
6972
* [`canon error-context.debug-message`](#-canon-error-contextdebug-message) 📝
7073
* [`canon error-context.drop`](#-canon-error-contextdrop) 📝
@@ -119,7 +122,7 @@ into the `handles` or `threads` fields of `ComponentInstance`.
119122
```python
120123
class ComponentInstance:
121124
store: Store
122-
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
125+
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext | Task]
123126
threads: Table[Thread]
124127
may_leave: bool
125128
backpressure: int
@@ -609,8 +612,7 @@ class Task:
609612
on_resolve: OnResolve
610613
state: State
611614
num_borrows: int
612-
implicit_thread: Optional[Thread]
613-
threads: list[Thread]
615+
waiting_to_enter: Optional[Thread]
614616

615617
def __init__(self, ft, opts, inst, on_start, on_resolve):
616618
self.ft = ft
@@ -620,22 +622,12 @@ class Task:
620622
self.on_resolve = on_resolve
621623
self.state = Task.State.INITIAL
622624
self.num_borrows = 0
623-
self.implicit_thread = None
624-
self.threads = []
625+
self.waiting_to_enter = None
625626
```
626627

627628
The `Task.needs_exclusive` predicate returns whether this task's implicit thread
628-
(`Task.implicit_thread`) has *not* opted in to multiple concurrent linear memory
629-
shadow stacks (via "stackful" lift) and thus, according to [Component Invariant]
630-
#2, requires serialization with all the other implicit threads in the component
631-
instance that have similarly not opted in. This question only applies to
632-
`async`-typed functions, since synchronous functions can't block and thus can
633-
always execute in a LIFO fashion using a single linear memory shadow stack. When
634-
`needs_exclusive` is true, core wasm execution is gated on acquiring the
635-
`ComponentInstance.exclusive_thread` lock. Due to cooperativity, the
636-
`exclusive_thread` "lock" is simply a mutable field holding either `None`, when
637-
unlocked, or, when locked, a reference to the `Task.implicit_thread` currently
638-
holding the lock.
629+
requires the `ComponentInstance.exclusive_thread` lock to acquired and released
630+
in order to enforce [Component Invariant] #2.
639631
```python
640632
def needs_exclusive(self):
641633
assert(self.ft.async_)
@@ -668,29 +660,24 @@ exports.
668660
```python
669661
def enter_implicit_thread(self):
670662
assert(self.state == Task.State.INITIAL)
671-
self.implicit_thread = current_thread()
672663
if self.ft.async_:
673664
def has_backpressure():
674665
return (self.inst.backpressure > 0 or
675666
(self.needs_exclusive() and self.inst.exclusive_thread is not None))
676667
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
668+
self.waiting_to_enter = current_thread()
677669
self.inst.num_waiting_to_enter += 1
678-
self.implicit_thread.wait_until(lambda: not has_backpressure())
670+
current_thread().wait_until(lambda: not has_backpressure())
679671
self.inst.num_waiting_to_enter -= 1
672+
self.waiting_to_enter = None
680673
if self.deliver_pending_cancel():
681674
self.cancel()
682675
return False
683676
if self.needs_exclusive():
684677
assert(self.inst.exclusive_thread is None)
685-
self.inst.exclusive_thread = self.implicit_thread
686-
self.register_thread(self.implicit_thread)
678+
self.inst.exclusive_thread = current_thread()
679+
current_thread().index = self.inst.threads.add(current_thread())
687680
return True
688-
689-
def register_thread(self, thread):
690-
assert(thread not in self.threads and thread.task is self)
691-
self.threads.append(thread)
692-
assert(thread.index is None)
693-
thread.index = self.inst.threads.add(thread)
694681
```
695682
Since the order in which suspended threads are resumed is nondeterministic (see
696683
`Store.tick` below), once `Task.enter_implicit_thread` suspends the task's
@@ -704,58 +691,44 @@ above definition ensures the following properties:
704691
backpressure (i.e., disabling backpressure never unleashes an unstoppable
705692
thundering herd of pending tasks).
706693

707-
Once a task's implicit thread has cleared the backpressure gate, it is added to
708-
the lists of threads running inside the current task and component instance by
709-
`Task.register_thread()` (which is also called by `thread.new-indirect`, below).
694+
As shown above, only once a task has cleared the backpressure gate is its
695+
implicit thread visibly added to the component-instance-wide `threads` table.
710696

711697
Symmetrically, the `Task.exit_implicit_thread` method is called before a task's
712698
implicit thread returns to reverse the effects of `Task.enter_implicit_thread`.
713-
In particular, if the `exclusive_thread` lock was acquired, it is released.
714-
`Task.unregister_thread` (which is also called by `thread.new-indirect`, below)
715-
traps if the task's last thread is unregistered and the task has not yet
716-
returned a value to its caller.
717699
```python
718700
def exit_implicit_thread(self):
719-
assert(current_thread() is self.implicit_thread)
720-
self.unregister_thread(self.implicit_thread)
701+
self.inst.threads.remove(current_thread().index)
721702
if self.ft.async_ and self.needs_exclusive():
722-
assert(self.inst.exclusive_thread is self.implicit_thread)
703+
assert(self.inst.exclusive_thread is current_thread())
723704
self.inst.exclusive_thread = None
724-
725-
def unregister_thread(self, thread):
726-
assert(thread in self.threads and thread.task is self)
727-
self.threads.remove(thread)
728-
if len(self.threads) == 0:
729-
trap_if(self.state != Task.State.RESOLVED)
730-
assert(self.num_borrows == 0)
731-
assert(thread.index is not None)
732-
self.inst.threads.remove(thread.index)
733705
```
734706

735707
The `Task.request_cancellation` method implements the `OnCancel` callback that
736708
is called by the `subtask.cancel` built-in. If a task's implicit thread is
737709
waiting to start (in `Task.enter_implicit_thread`, defined above) due to
738710
backpressure, then it is immediately cancelled without running any guest code.
739-
Otherwise, if the task's implicit thread is `cancellable` and `ready`, it is
740-
resumed, allowing it to execute until returning or blocking. Currently, only
741-
`callback` threads that have returned to their event loop are `cancellable`.
742-
When resumed, the `callback` thread will call `Task.deliver_pending_cancel`,
743-
which will return `True`, leading to the `TASK_CANCELLED` event code being
744-
passed to the `callback` function. Ideally, the `callback` function will
745-
promptly call `task.cancel` (or `task.return`) to resolve the task before the
746-
`OnCancel` callback returns, allowing `subtask.cancel` to return without
747-
blocking.
711+
Otherwise, if the task contains one or more threads that is `cancellable` and
712+
`ready`, one nondeterministically picked and resumed to allow it to execute
713+
until returning or blocking. Currently, only `callback` threads that have
714+
returned to their event loop are `cancellable`. When resumed, the `callback`
715+
thread will call `Task.deliver_pending_cancel`, which will return `True`,
716+
leading to the `TASK_CANCELLED` event code being passed to the `callback`
717+
function. Ideally, the `callback` function will promptly call `task.cancel` (or
718+
`task.return`) to resolve the task before the `OnCancel` callback returns,
719+
allowing `subtask.cancel` to return without blocking.
748720
```python
749721
def request_cancellation(self):
750722
if self.state == Task.State.INITIAL:
751723
self.state = Task.State.PENDING_CANCEL
752-
self.implicit_thread.resume()
724+
self.waiting_to_enter.resume()
753725
assert(self.state == Task.State.RESOLVED)
754726
else:
755727
assert(self.state == Task.State.STARTED)
756728
self.state = Task.State.PENDING_CANCEL
757-
if self.implicit_thread.cancellable and self.implicit_thread.ready():
758-
self.implicit_thread.resume()
729+
candidates = { t for t in self.inst.threads if t.task is self and t.cancellable and t.ready() }
730+
if candidates:
731+
random.choice(list(candidates)).resume()
759732

760733
def has_pending_cancel(self):
761734
return self.state == Task.State.PENDING_CANCEL
@@ -3374,10 +3347,12 @@ are lifted according to `lift_flat_values` above, the optional `post-return`
33743347
function (specified as a `canonopt` immediate of `canon lift`) is called,
33753348
passing the same core wasm results as parameters so that the `post-return`
33763349
function can free any associated allocations.
3350+
TODO: copy from set-task-old (and also alll the other stuff in Concurrency.md)
33773351
```python
33783352
if not opts.async_:
33793353
flat_results = call_and_trap_on_throw(callee, flat_args)
33803354
assert(types_match_values(flat_ft.results, flat_results))
3355+
trap_if(thread.task is not task)
33813356
result = lift_flat_values(cx, MAX_FLAT_RESULTS, CoreValueIter(flat_results), ft.result_type())
33823357
task.return_(result)
33833358
if opts.post_return is not None:
@@ -3394,11 +3369,11 @@ functions can always be implemented by a plain synchronous function call
33943369
without the need for fibers which would otherwise be necessary if the
33953370
`post-return` function performed a blocking operation.
33963371

3397-
In both of the `async` cases below (with or without `callback`), the
3398-
`task.return` built-in must be called, providing the return value as core wasm
3399-
*parameters* to the `task.return` built-in (rather than as core function
3400-
results as in the synchronous case). If `task.return` is *not* called by the
3401-
time the `Task`'s last `Thread` exits, there is a trap (in `Task.unregister_thread`).
3372+
In both of the `async` cases below (with or without `callback`), the return
3373+
value is provided by calling the `task.return` built-in, passing the return
3374+
value as core wasm *parameters* (rather than as core function results as in
3375+
the synchronous case). If `task.return` never ends up being called, the
3376+
task will never complete for the caller.
34023377

34033378
In the `async` non-`callback` ("stackful async") case, there is a single call
34043379
to the core wasm callee which must return empty core results. Waiting for async
@@ -3426,7 +3401,7 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
34263401
if thread.task.deliver_pending_cancel():
34273402
event = (EventCode.TASK_CANCELLED, 0, 0)
34283403
else:
3429-
assert(inst.exclusive_thread is task.implicit_thread)
3404+
assert(inst.exclusive_thread is thread)
34303405
inst.exclusive_thread = None
34313406
thread.cancellable = True
34323407
match code:
@@ -3444,7 +3419,7 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
34443419
trap()
34453420
assert(inst.exclusive_thread is None)
34463421
thread.cancellable = False
3447-
inst.exclusive_thread = task.implicit_thread
3422+
inst.exclusive_thread = thread
34483423
event_code, p1, p2 = event
34493424
[packed] = call_and_trap_on_throw(opts.callback, [event_code, p1, p2])
34503425
code,si = unpack_callback_result(packed)
@@ -4490,17 +4465,17 @@ class CoreFuncRef:
44904465
callee: Callable[[list[CoreValType]], list[CoreValType]]
44914466

44924467
def canon_thread_new_indirect(ft, ftbl: Table[CoreFuncRef], fi, c):
4493-
task = current_task()
4494-
trap_if(not task.inst.may_leave)
4468+
inst = current_instance()
4469+
trap_if(not inst.may_leave)
44954470
f = ftbl.get(fi)
44964471
assert(ft == CoreFuncType(['i32'], []) or ft == CoreFuncType(['i64'], []))
44974472
trap_if(f.t != ft)
44984473
def thread_func():
44994474
[] = call_and_trap_on_throw(f.callee, [c])
4500-
task.unregister_thread(new_thread)
4501-
new_thread = Thread(task, thread_func)
4475+
inst.threads.remove(new_thread.index)
4476+
new_thread = Thread(current_task(), thread_func)
45024477
assert(new_thread.suspended())
4503-
task.register_thread(new_thread)
4478+
new_thread.index = inst.threads.add(new_thread)
45044479
return [new_thread.index]
45054480
```
45064481
The newly-created thread starts out in a "suspended" state and so, to
@@ -4679,6 +4654,66 @@ def canon_thread_yield_then_promote(i):
46794654
```
46804655

46814656

4657+
###### 🧵 `canon thread.get-task`
4658+
4659+
For a canonical definition:
4660+
```wat
4661+
(canon thread.get-task (core func $thread.get-task))
4662+
```
4663+
validation specifies:
4664+
* `$thread.get-task` is given type `(func (result i32))`
4665+
4666+
Calling `$thread.get-task` invokes the following function which TODO
4667+
```python
4668+
def canon_thread_get_task():
4669+
thread = current_thread()
4670+
trap_if(not thread.task.inst.may_leave)
4671+
taski = thread.task.inst.handles.add(thread.task)
4672+
return [taski]
4673+
```
4674+
4675+
4676+
###### 🧵 `canon thread.set-task`
4677+
4678+
For a canonical definition:
4679+
```wat
4680+
(canon thread.set-task (core func $thread.set-task))
4681+
```
4682+
validation specifies:
4683+
* `$thread.set-task` is given type `(func (param i32))`
4684+
4685+
Calling `$thread.set-task` invokes the following function which TODO
4686+
```python
4687+
def canon_thread_set_task(taski):
4688+
thread = current_thread()
4689+
trap_if(not thread.task.inst.may_leave)
4690+
new_task = thread.task.inst.handles.get(taski)
4691+
trap_if(not isinstance(new_task, Task))
4692+
thread.task = new_task
4693+
return []
4694+
```
4695+
4696+
4697+
###### 🧵 `canon task.drop`
4698+
4699+
For a canonical definition:
4700+
```wat
4701+
(canon thread.drop (core func $task.drop))
4702+
```
4703+
validation specifies:
4704+
* `$thread.drop` is given type `(func (param i32))`
4705+
4706+
Calling `$task.drop` invokes the following function which TODO
4707+
```python
4708+
def canon_task_drop(taski):
4709+
inst = current_instance()
4710+
trap_if(not inst.may_leave)
4711+
task = inst.handles.remove(taski)
4712+
trap_if(not isinstance(task, Task))
4713+
return []
4714+
```
4715+
4716+
46824717
### 📝 `canon error-context.new`
46834718

46844719
For a canonical definition:

0 commit comments

Comments
 (0)