Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 47 additions & 28 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,27 +695,36 @@ report any pending cancellation if the caller is `cancellable`.

Lastly, the `Thread.suspend_then_promote` and `Thread.yield_then_promote`
methods *attempt* to immediately resume execution of some `other` thread in the
same component instance *if* the `other` thread is in a `ready` `waiting` state.
If so, control flow is transferred directly and the current thread is left
`suspended` or in a `ready` `waiting` state, resp. If the `other` thread is
*not* ready to run, then these operations fall back to plain `suspend` or
`yield_` behavior, resp.
same component instance *if* the `other` thread is "promotable", which means:
* the thread is in the `ready` state (not in the `suspended` state; this allows
one thread to give another thread in an unknown state a scheduling "boost"
with `pthread_join` being an example use case); and
* the thread does not assume exclusive use of a global linear memory shadow
stack (as defined below as part of `Task`).

If the `other` thread is indeed promotable, control flow is transferred directly
and the current thread is left `suspended` or in a `ready` `waiting` state,
resp. If the `other` thread is *not* promotable, then these operations fall back
to plain `suspend` or `yield_` behavior, resp.
```python
def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled:
assert(self.running())
if other.ready():
if other.promotable():
other.stop_waiting_internal(cancelled = False)
return self.suspend_then_resume(cancellable, other)
else:
return self.suspend(cancellable)

def yield_then_promote(self, cancellable, other: Thread) -> Cancelled:
assert(self.running())
if other.ready():
if other.promotable():
other.stop_waiting_internal(cancelled = False)
return self.yield_then_resume(cancellable, other)
else:
return self.yield_(cancellable)

def promotable(self):
return self.ready() and not Task.thread_needs_exclusive(self)
```


Expand Down Expand Up @@ -808,6 +817,19 @@ holding the lock.
return not self.opts.async_ or self.opts.callback
```

Building on this *task*-level definition, the following function defines when
a particular *thread* needs exclusive access to a global linear memory shadow
stack, which isn't simply `thread.task.needs_exclusive()` since, according to
[Component Invariant] #3, explicit threads can be assumed to have their own
stack and non-`async`-typed functions can be assumed to execute in a LIFO
fashion (on top of any existing stack):
```python
def thread_needs_exclusive(thread):
return (thread is thread.task.implicit_thread
and thread.task.ft.async_
and thread.task.needs_exclusive())
```

The `Task.enter_implicit_thread` method implements [backpressure] between when
the caller of an `async`-typed function initiates the call and when the callee's
core wasm entry point is executed. This interstitial placement allows a
Expand Down Expand Up @@ -3752,32 +3774,29 @@ calls `Thread.resume` on the new thread to synchronously transfer control flow
to it (jumping to the top of `thread_func` above). The new thread executes until
it either returns from `thread_func` or [blocks] by (transitively) calling
`Thread.block_internal`. If a non-`async`-typed call blocks before the implicit
thread has returned a value and there are no other `ready` threads in the same
component instance, `canon_lift` traps, since non-`async`-typed calls may not
block. Otherwise, `canon_lift` switches to a thread (nondeterministically, if
multiple are `ready`), as if the guest code had done so itself using a built-in
like `thread.suspend-then-promote`. This allows fully-synchronous components to
still use cooperative pthreads that interleave via threading built-ins (e.g.,
`thread.yield`) and *even perform blocking I/O* as long as the blocking I/O does
not transitively block returning a value to the caller (as would also be
expressible with a CPS transform like [Asyncify]). Lastly, `canon_lift` returns
`Task.request_cancellation`, bound to the call's new task, as the `OnCancel`
return value of `FuncInst`.
thread has returned a value and there are no "promotable" threads in the
component instance (with `Thread.promotable` as defined for the
`thread.{suspend,yield}-then-promote` built-ins above), `canon_lift` traps,
since non-`async`-typed calls may not block. Otherwise, `canon_lift` switches to
a promotable thread (nondeterministically, if there are multiple), as if the
guest code had done so itself using `thread.{suspend,yield}-then-promote`. This
allows fully-synchronous components to still use cooperative pthreads that
interleave via threading built-ins (e.g., `thread.yield`) and *even perform
blocking I/O* as long as the blocking I/O does not transitively block returning
a value to the caller (as would also be expressible with a CPS transform like
[Asyncify]). Lastly, `canon_lift` returns `Task.request_cancellation`, bound to
the call's new task, as the `OnCancel` return value of `FuncInst`.
```python
task = Task(ft, opts, inst, on_start, on_resolve)
thread = Thread(task, thread_func)
thread.resume()
if not ft.async_:
while task.state != Task.State.RESOLVED:
candidates = { t for t in inst.threads if t.ready() and t is not inst.exclusive_thread }
candidates = { t for t in inst.threads if t.promotable() }
trap_if(not candidates)
random.choice(list(candidates)).resume()
return task.request_cancellation
```
The special case that excludes any thread (created by a previous blocked `async`
call) holding the instance's `exclusive_thread` lock is necessary to preserve
[Component Invariant] #3, which might otherwise be violated if the current
synchronous call is using the single global linear memory shadow stack.

Note that, because non-`async`-typed functions can't block, they do not actually
require a separate thread/fiber/stack to implement the above specified behavior
Expand Down Expand Up @@ -5074,8 +5093,8 @@ validation specifies:

Calling `$suspend-then-promote` invokes the following function which loads a
thread at index `$i` from the current component instance's `threads` table and
then calls `Thread.suspend_then_resume` to resume the `other_thread` if it's
`ready` and, in any case, leave the [current thread] suspended.
resumes that thread if it's `Thread.promotable`, leaving the [current thread]
suspended in any case.
```python
def canon_thread_suspend_then_promote(cancellable, i):
thread = current_thread()
Expand Down Expand Up @@ -5103,9 +5122,9 @@ validation specifies:

Calling `$yield-then-promote` invokes the following function which loads a
thread at index `$i` from the current component instance's `threads` table and
then calls `Thread.yield_then_resume` to resume the `other_thread` if it's
`ready` and, in any case, leave the [current thread] ready to run at some
nondeterministic point in the future chosen by the embedder.
resumes that thread if it's `Thread.promotable`, leaving the [current thread]
ready to run at some nondeterministic point in the future chosen by the
embedder in any case.
```python
def canon_thread_yield_then_promote(cancellable, i):
thread = current_thread()
Expand Down
9 changes: 5 additions & 4 deletions design/mvp/Concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,11 @@ useful if a thread has a long-running computation without I/O but still needs to
allow other cooperative threads to make progress concurrently.

Lastly, in addition to being able to switch to "suspended" threads, threads can
also switch to threads that are in a "ready to run" state by calling the
[`thread.suspend-then-promote`] and [`thread.yield-then-promote`] built-ins
which, like the `thread.{suspend,yield}-then-resume` built-ins, leave the
calling thread in a "suspended" or "ready to run" state, resp. The calling
also switch to threads that are in a "ready to run" state and have no need
for exclusive access to a global shadow stack ([Component Invariant] #3) by
calling the [`thread.suspend-then-promote`] and [`thread.yield-then-promote`]
built-ins which, like the `thread.{suspend,yield}-then-resume` built-ins, leave
the calling thread in a "suspended" or "ready to run" state, resp. The calling
thread *may* know that the target thread is ready to run (e.g., because the
target thread is known to have yielded or to be waiting on a future/stream
operation that the calling thread just completed). However, in general,
Expand Down
26 changes: 14 additions & 12 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2271,10 +2271,11 @@ For details, see [Thread Built-ins] in the concurrency explainer and
| Canonical ABI signature | `[t:i32] -> [i32]` |

The `thread.suspend-then-promote` built-in immediately resumes execution of the
thread `t` if `t` is in a "ready" state, in any case leaving the current thread
in a "suspended" state. If `cancellable` is set, `thread.suspend-then-promote`
returns whether the current task was [cancelled] by the caller; otherwise,
`thread.suspend-then-promote` always returns `false`.
thread `t` if `t` is "ready" and has no requirements of exclusive access to a
global linear memory shadow stack ([Component Invariant] #3), in any case
leaving the current thread in a "suspended" state. If `cancellable` is set,
`thread.suspend-then-promote` returns whether the current task was [cancelled]
by the caller; otherwise, `thread.suspend-then-promote` always returns `false`.

For details, see [Thread Built-ins] in the concurrency explainer and
[`canon_thread_suspend_then_promote`] in the Canonical ABI explainer.
Expand All @@ -2287,10 +2288,11 @@ For details, see [Thread Built-ins] in the concurrency explainer and
| Canonical ABI signature | `[t:i32] -> [i32]` |

The `thread.yield-then-promote` built-in immediately resumes execution of the
thread `t` if `t` is in a "ready" state, in any case leaving the current thread
in a "ready" state. If `cancellable` is set, `thread.yield-then-promote` returns
whether the current task was [cancelled] by the caller; otherwise,
`thread.yield-then-promote` always returns `false`.
thread `t` if `t` is "ready" and has no requirements of exclusive access to a
global linear memory shadow stack ([Component Invariant] #3), in any case
leaving the current thread in a "ready" state. If `cancellable` is set,
`thread.yield-then-promote` returns whether the current task was [cancelled] by
the caller; otherwise, `thread.yield-then-promote` always returns `false`.

For details, see [Thread Built-ins] in the concurrency explainer and
[`canon_thread_yield_then_promote`] in the Canonical ABI explainer.
Expand Down Expand Up @@ -3005,10 +3007,9 @@ In particular, the Component Model maintains the following invariants:
restriction in an explicit opt-in manner.)

3. To ease adoption, unless a component opts in (via "stackful" lift 🚟 or
cooperative threads 🧵), all core wasm execution inside a component instance
is locally serialized (via automatic backpressure applied at export calls) so
that producer toolchains can continue to use a single global linear memory
shadow stack that is pushed and popped in LIFO order.
cooperative threads 🧵), all core wasm inside a component instance executes
in a LIFO manner so that producer toolchains can continue to use a single
global linear memory shadow stack that is pushed and popped in LIFO order.


## JavaScript Embedding
Expand Down Expand Up @@ -3336,6 +3337,7 @@ For some use-case-focused, worked examples, see:
[GC ABI Option]: https://github.com/WebAssembly/component-model/issues/525

[Strongly-unique]: #name-uniqueness
[Component Invariant]: #component-invariants

[Donut Wrapped]: Linking.md#higher-order-shared-nothing-linking-aka-donut-wrapping
[Adapter Functions]: FutureFeatures.md#custom-abis-via-adapter-functions
Expand Down
14 changes: 11 additions & 3 deletions design/mvp/canonical-abi/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,20 +420,23 @@ def yield_then_resume(self, cancellable, other: Thread) -> Cancelled:

def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled:
assert(self.running())
if other.ready():
if other.promotable():
other.stop_waiting_internal(cancelled = False)
return self.suspend_then_resume(cancellable, other)
else:
return self.suspend(cancellable)

def yield_then_promote(self, cancellable, other: Thread) -> Cancelled:
assert(self.running())
if other.ready():
if other.promotable():
other.stop_waiting_internal(cancelled = False)
return self.yield_then_resume(cancellable, other)
else:
return self.yield_(cancellable)

def promotable(self):
return self.ready() and not Task.thread_needs_exclusive(self)

### Tasks

OnStart = Callable[[], list[any]]
Expand Down Expand Up @@ -474,6 +477,11 @@ def needs_exclusive(self):
assert(self.ft.async_)
return not self.opts.async_ or self.opts.callback

def thread_needs_exclusive(thread):
return (thread is thread.task.implicit_thread
and thread.task.ft.async_
and thread.task.needs_exclusive())

def enter_implicit_thread(self):
assert(self.state == Task.State.INITIAL)
self.implicit_thread = current_thread()
Expand Down Expand Up @@ -2212,7 +2220,7 @@ def thread_func():
thread.resume()
if not ft.async_:
while task.state != Task.State.RESOLVED:
candidates = { t for t in inst.threads if t.ready() and t is not inst.exclusive_thread }
candidates = { t for t in inst.threads if t.promotable() }
trap_if(not candidates)
random.choice(list(candidates)).resume()
return task.request_cancellation
Expand Down
101 changes: 101 additions & 0 deletions design/mvp/canonical-abi/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2949,6 +2949,106 @@ def on_resolve(v):
assert(result == 42)
assert(other_result == 43)

def test_promotable():
store = Store()
inst = ComponentInstance(store)

run_done = False
cb_threadi = None
cb_exited = False
cb_ft = FuncType([], [U32Type()], async_ = True)
cb_opts = mk_opts(async_ = True)
def core_cb(args):
assert(not args)
nonlocal cb_threadi
[cb_threadi] = canon_thread_index()
[] = canon_task_return([U32Type()], cb_opts, [1])
return [CallbackCode.YIELD]
def core_cb_callback(args):
[event,payload1,payload2] = args
assert(event == EventCode.NONE and payload1 == 0 and payload2 == 0)
assert(run_done)
nonlocal cb_exited
cb_exited = True
return [CallbackCode.EXIT]
cb_opts.callback = core_cb_callback
cb_result = None
def on_cb_resolve(v):
nonlocal cb_result
[cb_result] = v
_ = store.invoke(store.lift(core_cb, cb_ft, cb_opts, inst), lambda:[], on_cb_resolve)
assert(cb_result == 1)

sf_ft = FuncType([], [U32Type()], async_ = True)
sf_opts = mk_opts(async_ = True)

sf1_done = False
def core_sf1(args):
assert(not args)
[] = canon_task_return([U32Type()], sf_opts, [2])
[ret] = canon_thread_yield(False)
assert(ret == Cancelled.FALSE)
nonlocal sf1_done
sf1_done = True
return []
sf1_result = None
def on_sf1_resolve(v):
nonlocal sf1_result
[sf1_result] = v
_ = store.invoke(store.lift(core_sf1, sf_ft, sf_opts, inst), lambda:[], on_sf1_resolve)
assert(sf1_result == 2)
assert(not sf1_done)

sf2_threadi = None
sf2_done = False
def core_sf2(args):
assert(not args)
nonlocal sf2_threadi
[sf2_threadi] = canon_thread_index()
[] = canon_task_return([U32Type()], sf_opts, [3])
[ret] = canon_thread_suspend(False)
assert(ret == Cancelled.FALSE)
nonlocal sf2_done
sf2_done = True
return []
sf2_result = None
def on_sf2_resolve(v):
nonlocal sf2_result
[sf2_result] = v
_ = store.invoke(store.lift(core_sf2, sf_ft, sf_opts, inst), lambda:[], on_sf2_resolve)
assert(sf2_result == 3)
assert(not sf2_done)

def core_run(args):
assert(not args)

while not sf1_done:
[ret] = canon_thread_yield(False)
assert(ret == Cancelled.FALSE)

[] = canon_thread_resume_later(sf2_threadi)
assert(not sf2_done)
[ret] = canon_thread_yield_then_promote(False, sf2_threadi)
assert(ret == Cancelled.FALSE)
assert(sf2_done)

for _ in range(10):
[ret] = canon_thread_yield_then_promote(False, cb_threadi)
assert(ret == Cancelled.FALSE)

return [42]

run_result = None
def on_run_resolve(v):
nonlocal run_result, run_done
[run_result] = v
run_done = True

caller_ft = FuncType([], [U8Type()])
lift_and_run(mk_opts(), inst, caller_ft, core_run, lambda:[], on_run_resolve)
assert(run_result == 42)
assert(cb_exited)

def test_thread_cancel_callback():
store = Store()
root_inst = ComponentInstance(store)
Expand Down Expand Up @@ -3046,6 +3146,7 @@ def core_consumer(args):
test_async_flat_params()
test_threads()
test_sync_threads()
test_promotable()
test_thread_cancel_callback()

print("All tests passed")
2 changes: 1 addition & 1 deletion test/async/during-sync-call-no-exclusive-resume.wast
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,4 @@
(canon lift (core func $core "sync-block")))
)
(assert_return (invoke "arm") (u32.const 1))
(assert_trap (invoke "sync-block") "deadlock detected: event loop cannot make further progress")
(assert_trap (invoke "sync-block") "cannot block a synchronous task before returning")
2 changes: 1 addition & 1 deletion test/async/during-sync-call-no-sibling-resume.wast
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,4 @@
(func (export "sync-block") (alias export $inner "sync-block"))
)
(assert_return (invoke "arm"))
(assert_trap (invoke "sync-block") "deadlock detected: event loop cannot make further progress")
(assert_trap (invoke "sync-block") "cannot block a synchronous task before returning")
Loading
Loading