Skip to content

Commit a7e85fa

Browse files
committed
CABI: fix may_block to not use the current task
1 parent 40a9956 commit a7e85fa

File tree

3 files changed

+290
-72
lines changed

3 files changed

+290
-72
lines changed

design/mvp/CanonicalABI.md

Lines changed: 95 additions & 52 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
@@ -332,6 +362,7 @@ class ComponentInstance:
332362
handles: Table[ResourceHandle | Waitable | WaitableSet | ErrorContext]
333363
threads: Table[Thread]
334364
may_leave: bool
365+
may_block: bool
335366
backpressure: int
336367
exclusive: Optional[Task]
337368
num_waiting_to_enter: int
@@ -343,6 +374,7 @@ class ComponentInstance:
343374
self.handles = Table()
344375
self.threads = Table()
345376
self.may_leave = True
377+
self.may_block = True
346378
self.backpressure = 0
347379
self.exclusive = None
348380
self.num_waiting_to_enter = 0
@@ -883,7 +915,7 @@ here which transfers control flow back to `Thread.resume()` via `block()`. The
883915
`switch_to` argument `None` tells `Thread.resume()` to return immediately.
884916
```python
885917
def suspend(self, cancellable) -> Cancelled:
886-
assert(self.running() and self.task.may_block())
918+
assert(self.running() and self.task.inst.may_block)
887919
if self.task.deliver_pending_cancel(cancellable):
888920
return Cancelled.TRUE
889921
self.cancellable = cancellable
@@ -905,7 +937,7 @@ until a particular condition is met, as specified by the boolean-valued
905937
`ready_func` parameter:
906938
```python
907939
def wait_until(self, ready_func, cancellable = False) -> Cancelled:
908-
assert(self.running() and self.task.may_block())
940+
assert(self.running() and self.task.inst.may_block)
909941
if self.task.deliver_pending_cancel(cancellable):
910942
return Cancelled.TRUE
911943
if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0,1):
@@ -932,7 +964,7 @@ emulating preemptive multi-threading.
932964
```python
933965
def yield_until(self, ready_func, cancellable) -> Cancelled:
934966
assert(self.running())
935-
if self.task.may_block():
967+
if self.task.inst.may_block:
936968
return self.wait_until(ready_func, cancellable)
937969
else:
938970
assert(ready_func())
@@ -1177,14 +1209,6 @@ synchronously or with `async callback`. This predicate is used by the other
11771209
return not self.opts.async_ or self.opts.callback
11781210
```
11791211

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

@@ -1323,15 +1357,19 @@ by `Task.deliver_pending_cancel`, which is checked at all cancellation points:
13231357

13241358
The `Task.return_` method is called by `canon_task_return` and `canon_lift` to
13251359
return a list of lifted values to the task's caller via the `OnResolve`
1326-
callback. There is a dynamic error if the callee has not dropped all borrowed
1327-
handles by the time `task.return` is called which means that the caller can
1328-
assume that all its lent handles have been returned to it when it receives the
1329-
`SUBTASK` `RETURNED` event. Note that the initial `trap_if` allows a task to
1360+
callback. After returning a value, a synchronous (non-`async`-typed) function
1361+
is allowed to block. There is a dynamic error if the callee has not dropped all
1362+
borrowed handles by the time `task.return` is called which means that the caller
1363+
can assume that all its lent handles have been returned to it when it receives
1364+
the `SUBTASK` `RETURNED` event. Note that the initial `trap_if` allows a task to
13301365
return a value even after cancellation has been requested.
13311366
```python
13321367
def return_(self, result):
13331368
trap_if(self.state == Task.State.RESOLVED)
13341369
trap_if(self.num_borrows > 0)
1370+
if not self.ft.async_:
1371+
assert(not self.inst.may_block)
1372+
self.inst.may_block = True
13351373
assert(result is not None)
13361374
self.on_resolve(result)
13371375
self.state = Task.State.RESOLVED
@@ -1349,6 +1387,7 @@ call `task.cancel`.
13491387
def cancel(self):
13501388
trap_if(self.state != Task.State.CANCEL_DELIVERED)
13511389
trap_if(self.num_borrows > 0)
1390+
assert(self.ft.async_)
13521391
self.on_resolve(None)
13531392
self.state = Task.State.RESOLVED
13541393
```
@@ -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
@@ -5113,7 +5156,7 @@ def canon_thread_available_parallelism():
51135156
[Current Thread]: Concurrency.md#current-thread-and-task
51145157
[Current Task]: Concurrency.md#current-thread-and-task
51155158
[Blocks]: Concurrency.md#blocking
5116-
[Block]: Concurrency.md#blocking
5159+
[Blocking]: Concurrency.md#blocking
51175160
[Waiting On External I/O And Yielding]: Concurrency.md#blocking
51185161
[Subtasks]: Concurrency.md#subtasks-and-supertasks
51195162
[Readable and Writable Ends]: Concurrency.md#streams-and-futures

0 commit comments

Comments
 (0)