Skip to content

Commit 0bbf512

Browse files
committed
CABI: fix may_block to not use the current task
1 parent 1550d80 commit 0bbf512

File tree

3 files changed

+290
-71
lines changed

3 files changed

+290
-71
lines changed

design/mvp/CanonicalABI.md

Lines changed: 95 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -119,41 +119,71 @@ runtime behavior of the Canonical ABI, the Embedding interface here just
119119
includes functions for the embedder to:
120120
1. construct a Component Model `Store`, analogous to [`store_init`]ing a Core
121121
WebAssembly [`store`];
122-
2. `invoke` a Component Model `FuncInst`, analogous to [`func_invoke`]ing a
123-
Core WebAssembly [`funcinst`]; and
124-
3. allow a cooperative thread (created during a previous call to to `invoke`)
125-
to execute until blocking or exiting.
126-
122+
2. `Store.invoke` a Component Model `FuncInst`, analogous to [`func_invoke`]ing
123+
a Core WebAssembly [`funcinst`]; and
124+
3. allow an existing thread to execute, via `Store.tick`, until [blocking] or
125+
exiting.
126+
127+
`Store.tick` does not have an analogue in Core WebAssembly and is necessary to
128+
provide native [concurrency] support in the Component Model. The expectation is
129+
that the host will interleave calls to `invoke` with calls to `tick`, repeatedly
130+
calling `tick` until there is no more work to do or the store is destroyed.
131+
`tick` never blocks: if execution *would* block, the thread is suspended and
132+
execution returns to the host from `tick` (which can then call `invoke` or
133+
`tick` again, etc). Although the host would use more sophisticated data
134+
structures for performance, the definition of `tick` below simply tracks all the
135+
threads that are `waiting` to run once some `ready` condition is met in one big
136+
list and then nondeterministically picks one. The `Thread.ready` and
137+
`Thread.resume` methods along with how the `Store.waiting` list is populated are
138+
all defined below as part of the `Thread` class.
139+
140+
Hosts are allowed to call `Store.invoke` recursively, i.e., if a component is
141+
instantiated with host functions, the host is allowed to recursively call
142+
`Store.invoke` again, producing a synchronous callstack:
143+
```
144+
[host] --invoke--> [component] --call-import--> [host] --invoke--> [component]
145+
```
146+
Hosts are *not* allowed to call `Store.tick` recursively; it can only be called
147+
from the base of the call stack. However, once component code is running from
148+
a `Store.tick`, the host can recursively call `Store.invoke` as above:
149+
```
150+
[host] --tick--> [component] --call-import--> [host] --invoke--> [component]
151+
```
152+
Furthermore, hosts are required to link the two `[component]` calls in the above
153+
call stacks together, by passing the `Supertask` of the older `[component]` call
154+
to the inner `Store.invoke`.
155+
156+
The above host requirements are `assert()`ed by the `nesting_depth` accounting
157+
below which, depending on the embedding scenario, may or may not need to be
158+
enforced at runtime by the engine.
127159
```python
128160
class Store:
129161
waiting: list[Thread]
162+
nesting_depth: int
130163

131164
def __init__(self):
132165
self.waiting = []
166+
self.nesting_depth = 0
133167

134168
def invoke(self, f: FuncInst, caller: Optional[Supertask], on_start, on_resolve) -> Call:
135169
host_caller = Supertask()
136170
host_caller.inst = None
137171
host_caller.supertask = caller
138-
return f(host_caller, on_start, on_resolve)
172+
self.nesting_depth += 1
173+
call = f(host_caller, on_start, on_resolve)
174+
self.nesting_depth -= 1
175+
return call
139176

140177
def tick(self):
178+
assert(self.nesting_depth == 0)
179+
self.nesting_depth = 1
141180
random.shuffle(self.waiting)
142181
for thread in self.waiting:
143182
if thread.ready():
144183
thread.resume(Cancelled.FALSE)
145-
return
146-
```
147-
The `Store.tick` method does not have an analogue in Core WebAssembly and
148-
enables [native concurrency support](Concurrency.md) in the Component Model. The
149-
expectation is that the host will interleave calls to `invoke` with calls to
150-
`tick`, repeatedly calling `tick` until there is no more work to do or the
151-
store is destroyed. The nondeterministic `random.shuffle` indicates that the
152-
embedder is allowed to use any algorithm (involving priorities, fairness, etc)
153-
to choose which thread to schedule next (and hopefully an algorithm more
154-
efficient than the simple polling loop written above). The `Thread.ready` and
155-
`Thread.resume` methods along with how the `waiting` list is populated are all
156-
defined [below](#thread-state) as part of the `Thread` class.
184+
break
185+
self.nesting_depth = 0
186+
```
157187

158188
The `FuncInst` passed to `Store.invoke` is defined to take 3 parameters:
159189
* an optional `caller` `Supertask` which is used to maintain the
@@ -330,6 +360,7 @@ class ComponentInstance:
330360
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
331361
threads: Table[Thread]
332362
may_leave: bool
363+
may_block: bool
333364
backpressure: int
334365
exclusive: Optional[Task]
335366
num_waiting_to_enter: int
@@ -341,6 +372,7 @@ class ComponentInstance:
341372
self.handles = Table()
342373
self.threads = Table()
343374
self.may_leave = True
375+
self.may_block = True
344376
self.backpressure = 0
345377
self.exclusive = None
346378
self.num_waiting_to_enter = 0
@@ -881,7 +913,7 @@ here which transfers control flow back to `Thread.resume()` via `block()`. The
881913
`switch_to` argument `None` tells `Thread.resume()` to return immediately.
882914
```python
883915
def suspend(self, cancellable) -> Cancelled:
884-
assert(self.running() and self.task.may_block())
916+
assert(self.running() and self.task.inst.may_block)
885917
if self.task.deliver_pending_cancel(cancellable):
886918
return Cancelled.TRUE
887919
self.cancellable = cancellable
@@ -903,7 +935,7 @@ until a particular condition is met, as specified by the boolean-valued
903935
`ready_func` parameter:
904936
```python
905937
def wait_until(self, ready_func, cancellable = False) -> Cancelled:
906-
assert(self.running() and self.task.may_block())
938+
assert(self.running() and self.task.inst.may_block)
907939
if self.task.deliver_pending_cancel(cancellable):
908940
return Cancelled.TRUE
909941
if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0,1):
@@ -930,7 +962,7 @@ emulating preemptive multi-threading.
930962
```python
931963
def yield_until(self, ready_func, cancellable) -> Cancelled:
932964
assert(self.running())
933-
if self.task.may_block():
965+
if self.task.inst.may_block:
934966
return self.wait_until(ready_func, cancellable)
935967
else:
936968
assert(ready_func())
@@ -1175,14 +1207,6 @@ synchronously or with `async callback`. This predicate is used by the other
11751207
return not self.opts.async_ or self.opts.callback
11761208
```
11771209

1178-
The `Task.may_block` predicate returns whether the [current task]'s function's
1179-
type is allowed to [block]. Specifically, functions that do not declare the
1180-
`async` effect that have not yet returned a value may not block.
1181-
```python
1182-
def may_block(self):
1183-
return self.ft.async_ or self.state == Task.State.RESOLVED
1184-
```
1185-
11861210
The `Task.enter` method implements [backpressure] between when the caller of an
11871211
`async`-typed function initiates the call and when the callee's core wasm entry
11881212
point is executed. This interstitial placement allows a component instance that
@@ -1204,7 +1228,14 @@ they may reenter core wasm when an `async`-typed function would have been
12041228
blocked by implicit backpressure. Thus, export bindings generators must be
12051229
careful to handle this possibility (e.g., while maintaining the linear-memory
12061230
shadow stack pointer) for components with mixed `async`- and non-`async`- typed
1207-
exports.
1231+
exports. However, non-`async`-typed functions *must* return a value before
1232+
blocking, and thus the `may_block` flag (which is dynamically checked before
1233+
all Canonical ABI built-ins which might block) is cleared when entering a
1234+
non-`async`-typed function, to be cleared in `Task.return_`. Because recursive
1235+
calls to `Store.tick` and to the same component instance are disallowed (see
1236+
`Store` and `call_might_be_recursive`, resp.) and because reentrance is only
1237+
otherwise possible when blocked, `may_block` must always already be true when
1238+
`Task.enter` is called.
12081239
```python
12091240
def enter(self):
12101241
thread = current_thread()
@@ -1223,6 +1254,9 @@ exports.
12231254
if self.needs_exclusive():
12241255
assert(self.inst.exclusive is None)
12251256
self.inst.exclusive = self
1257+
else:
1258+
assert(self.inst.may_block)
1259+
self.inst.may_block = False
12261260
self.register_thread(thread)
12271261
return True
12281262

@@ -1321,15 +1355,19 @@ by `Task.deliver_pending_cancel`, which is checked at all cancellation points:
13211355

13221356
The `Task.return_` method is called by `canon_task_return` and `canon_lift` to
13231357
return a list of lifted values to the task's caller via the `OnResolve`
1324-
callback. There is a dynamic error if the callee has not dropped all borrowed
1325-
handles by the time `task.return` is called which means that the caller can
1326-
assume that all its lent handles have been returned to it when it receives the
1327-
`SUBTASK` `RETURNED` event. Note that the initial `trap_if` allows a task to
1358+
callback. After returning a value, a synchronous (non-`async`-typed) function
1359+
is allowed to block. There is a dynamic error if the callee has not dropped all
1360+
borrowed handles by the time `task.return` is called which means that the caller
1361+
can assume that all its lent handles have been returned to it when it receives
1362+
the `SUBTASK` `RETURNED` event. Note that the initial `trap_if` allows a task to
13281363
return a value even after cancellation has been requested.
13291364
```python
13301365
def return_(self, result):
13311366
trap_if(self.state == Task.State.RESOLVED)
13321367
trap_if(self.num_borrows > 0)
1368+
if not self.ft.async_:
1369+
assert(not self.inst.may_block)
1370+
self.inst.may_block = True
13331371
assert(result is not None)
13341372
self.on_resolve(result)
13351373
self.state = Task.State.RESOLVED
@@ -1347,6 +1385,7 @@ call `task.cancel`.
13471385
def cancel(self):
13481386
trap_if(self.state != Task.State.CANCEL_DELIVERED)
13491387
trap_if(self.num_borrows > 0)
1388+
assert(self.ft.async_)
13501389
self.on_resolve(None)
13511390
self.state = Task.State.RESOLVED
13521391
```
@@ -3517,7 +3556,7 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
35173556
else:
35183557
event = (EventCode.NONE, 0, 0)
35193558
case CallbackCode.WAIT:
3520-
trap_if(not task.may_block())
3559+
trap_if(not inst.may_block)
35213560
wset = inst.handles.get(si)
35223561
trap_if(not isinstance(wset, WaitableSet))
35233562
event = wset.wait_until(lambda: not inst.exclusive, cancellable = True)
@@ -3555,16 +3594,19 @@ tasks, which entirely ignore `ComponentInstance.exclusive`.
35553594
The end of `canon_lift` immediately runs the `thread_func` function (which
35563595
contains all the steps above) in a new `Thread`, allowing `thread_func` to make
35573596
as much progress as it can before blocking (which transitively calls
3558-
`Thread.suspend`, deterministically returning control flow here and then to the
3597+
`block`, deterministically returning control flow here and then to the
35593598
caller. If `thread_func` and the core wasm `callee` return a value (by calling
35603599
the `OnResolve` callback) before blocking, the call will complete synchronously
3561-
even for `async` callers. Note that if an `async` callee calls `OnResolve` and
3562-
*then* blocks, the caller will see the call complete synchronously even though
3563-
the callee is still running concurrently in the `Thread` created here (see
3564-
the [concurrency explainer] for more on this).
3600+
from the perspective of the caller (even if the callee then blocks *after*
3601+
returning a value, taking advantage of the fact that the callee is running on
3602+
its own `Thread`). By ABI contract, a callee can only block before returning
3603+
its value if it declares the `async` effect on the component function type. If a
3604+
callee is non-`async`-typed and *tries* to block, the callee will trap on the
3605+
`ComponentInstance.may_block` guards that dominate all calls to `block`.
35653606
```python
35663607
thread = Thread(task, thread_func)
35673608
thread.resume(Cancelled.FALSE)
3609+
assert(ft.async_ or task.state == Task.State.RESOLVED)
35683610
return task
35693611
```
35703612

@@ -3631,7 +3673,7 @@ this, `canon_lower` is defined in chunks as follows:
36313673
def canon_lower(opts, ft, callee: FuncInst, flat_args):
36323674
thread = current_thread()
36333675
trap_if(not thread.task.inst.may_leave)
3634-
trap_if(not thread.task.may_block() and ft.async_ and not opts.async_)
3676+
trap_if(not thread.task.inst.may_block and ft.async_ and not opts.async_)
36353677
```
36363678
A non-`async`-typed function export that has not yet returned a value
36373679
unconditionally traps if it transitively attempts to make a synchronous call to
@@ -4095,13 +4137,13 @@ on a `Waitable` in the given waitable set (indicated by index `$si`) and then
40954137
returning its `EventCode` and writing the payload values into linear memory:
40964138
```python
40974139
def canon_waitable_set_wait(cancellable, mem, si, ptr):
4098-
task = current_thread().task
4099-
trap_if(not task.inst.may_leave)
4100-
trap_if(not task.may_block())
4101-
wset = task.inst.handles.get(si)
4140+
inst = current_thread().task.inst
4141+
trap_if(not inst.may_leave)
4142+
trap_if(not inst.may_block)
4143+
wset = inst.handles.get(si)
41024144
trap_if(not isinstance(wset, WaitableSet))
41034145
event = wset.wait(cancellable)
4104-
return unpack_event(mem, task.inst, ptr, event)
4146+
return unpack_event(mem, inst, ptr, event)
41054147

41064148
def unpack_event(mem, inst, ptr, e: EventTuple):
41074149
event, p1, p2 = e
@@ -4245,7 +4287,7 @@ BLOCKED = 0xffff_ffff
42454287
def canon_subtask_cancel(async_, i):
42464288
thread = current_thread()
42474289
trap_if(not thread.task.inst.may_leave)
4248-
trap_if(not thread.task.may_block() and not async_)
4290+
trap_if(not thread.task.inst.may_block and not async_)
42494291
subtask = thread.task.inst.handles.get(i)
42504292
trap_if(not isinstance(subtask, Subtask))
42514293
trap_if(subtask.resolve_delivered())
@@ -4379,7 +4421,7 @@ whether the operation would have succeeded eagerly without blocking).
43794421
def stream_copy(EndT, BufferT, event_code, stream_t, opts, i, ptr, n):
43804422
thread = current_thread()
43814423
trap_if(not thread.task.inst.may_leave)
4382-
trap_if(not thread.task.may_block() and not opts.async_)
4424+
trap_if(not thread.task.inst.may_block and not opts.async_)
43834425
```
43844426

43854427
Next, `stream_copy` checks that the element at index `i` is of the right type
@@ -4501,7 +4543,7 @@ parameters `i` and `ptr`. The only difference is that, with futures, the
45014543
def future_copy(EndT, BufferT, event_code, future_t, opts, i, ptr):
45024544
thread = current_thread()
45034545
trap_if(not thread.task.inst.may_leave)
4504-
trap_if(not thread.task.may_block() and not opts.async_)
4546+
trap_if(not thread.task.inst.may_block and not opts.async_)
45054547

45064548
e = thread.task.inst.handles.get(i)
45074549
trap_if(not isinstance(e, EndT))
@@ -4587,7 +4629,7 @@ def canon_future_cancel_write(future_t, async_, i):
45874629
def cancel_copy(EndT, event_code, stream_or_future_t, async_, i):
45884630
thread = current_thread()
45894631
trap_if(not thread.task.inst.may_leave)
4590-
trap_if(not thread.task.may_block() and not async_)
4632+
trap_if(not thread.task.inst.may_block and not async_)
45914633
e = thread.task.inst.handles.get(i)
45924634
trap_if(not isinstance(e, EndT))
45934635
trap_if(e.shared.t != stream_or_future_t.t)
@@ -4783,7 +4825,7 @@ calling component.
47834825
def canon_thread_suspend(cancellable):
47844826
thread = current_thread()
47854827
trap_if(not thread.task.inst.may_leave)
4786-
trap_if(not thread.task.may_block())
4828+
trap_if(not thread.task.inst.may_block)
47874829
cancelled = thread.suspend(cancellable)
47884830
return [cancelled]
47894831
```
@@ -5102,6 +5144,7 @@ def canon_thread_available_parallelism():
51025144
[JavaScript Embedding]: Explainer.md#JavaScript-embedding
51035145
[Adapter Functions]: FutureFeatures.md#custom-abis-via-adapter-functions
51045146
[Shared-Everything Dynamic Linking]: examples/SharedEverythingDynamicLinking.md
5147+
[Concurrency]: Concurrency.md
51055148
[Concurrency Explainer]: Concurrency.md
51065149
[Suspended]: Concurrency.md#thread-built-ins
51075150
[Thread Index]: Concurrency.md#thread-built-ins
@@ -5112,6 +5155,7 @@ def canon_thread_available_parallelism():
51125155
[Thread]: Concurrency.md#threads-and-tasks
51135156
[Current Thread]: Concurrency.md#current-thread-and-task
51145157
[Current Task]: Concurrency.md#current-thread-and-task
5158+
[Blocking]: Concurrency.md#blocking
51155159
[Block]: Concurrency.md#blocking
51165160
[Waiting On External I/O And Yielding]: Concurrency.md#blocking
51175161
[Subtasks]: Concurrency.md#subtasks-and-supertasks

0 commit comments

Comments
 (0)