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
108 changes: 70 additions & 38 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class ComponentInstance:
threads: Table[Thread]
may_enter: bool
may_leave: bool
sync_depth: int
backpressure: int
num_waiting_to_enter: int
exclusive_thread: Optional[Thread]
Expand All @@ -138,6 +139,7 @@ class ComponentInstance:
self.threads = Table()
self.may_enter = True
self.may_leave = True
self.sync_depth = 0
self.backpressure = 0
self.num_waiting_to_enter = 0
self.exclusive_thread = None
Expand Down Expand Up @@ -695,23 +697,27 @@ 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" (as defined in
the next section by `Task.promotable`); otherwise these operations fall back to
plain `suspend` or `yield_` behavior, resp. This allows one thread to give
another thread in an unknown state a scheduling "boost" (with `pthread_join`
being an example use case).
```python
def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled:
assert(self.running())
if other.ready():
if self.task.deliver_pending_cancel(cancellable):
return Cancelled.TRUE
if current_task().promotable(other):
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 self.task.deliver_pending_cancel(cancellable):
return Cancelled.TRUE
if current_task().promotable(other):
other.stop_waiting_internal(cancelled = False)
return self.yield_then_resume(cancellable, other)
else:
Expand Down Expand Up @@ -808,6 +814,21 @@ holding the lock.
return not self.opts.async_ or self.opts.callback
```

Building on this, the `Task.promotable` predicate defines when a `ready` thread
can be resumed without violating [Component Invariant] #3 via either the
`Thread.{suspend,yield}_then_promote` methods or the synchronous thread
scheduling performed in `canon_lift` below. In particular, explicit threads, the
implicit thread of the current non-`async`-typed task (passed as `self`), and
the implicit threads of stackful `async`-typed tasks are all "promotable" if
they are in the `ready` state.
```python
def promotable(self, thread):
return (thread.ready()
and (thread is not thread.task.implicit_thread
or (not thread.task.ft.async_ and thread.task is self)
or (thread.task.ft.async_ and not 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 All @@ -819,7 +840,10 @@ of backpressure:
`backpressure.{inc,dec}` which modify the `ComponentInstance.backpressure`
counter.
2. *Implicit backpressure* triggered when `Task.needs_exclusive()` is true and
the `ComponentInstance.exclusive_thread` lock is already held.
either the `ComponentInstance.exclusive_thread` lock is already held *or*,
in a [donut wrapping] scenario, a parent's `async` function is being called
by a child component's import while the parent has a non-`async` call
already on the stack.
3. *Residual backpressure* triggered by explicit or implicit backpressure
having been enabled then disabled, but there still being tasks waiting to
enter that need to be given the chance to start without getting starved
Expand All @@ -837,8 +861,10 @@ exports.
self.implicit_thread = current_thread()
if self.ft.async_:
def has_backpressure():
return (self.inst.backpressure > 0 or
(self.needs_exclusive() and self.inst.exclusive_thread is not None))
return (self.inst.backpressure > 0
or (self.needs_exclusive()
and (self.inst.exclusive_thread is not None
or self.inst.sync_depth > 0)))
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
self.inst.num_waiting_to_enter += 1
cancelled = self.implicit_thread.wait_until(lambda: not has_backpressure(), cancellable = True)
Expand All @@ -849,6 +875,8 @@ exports.
if self.needs_exclusive():
assert(self.inst.exclusive_thread is None)
self.inst.exclusive_thread = self.implicit_thread
else:
self.inst.sync_depth += 1
self.register_thread(self.implicit_thread)
return True

Expand Down Expand Up @@ -884,9 +912,12 @@ returned a value to its caller.
def exit_implicit_thread(self):
assert(current_thread() is self.implicit_thread)
self.unregister_thread(self.implicit_thread)
if self.ft.async_ and self.needs_exclusive():
assert(self.inst.exclusive_thread is self.implicit_thread)
self.inst.exclusive_thread = None
if self.ft.async_:
if self.needs_exclusive():
assert(self.inst.exclusive_thread is self.implicit_thread)
self.inst.exclusive_thread = None
else:
self.inst.sync_depth -= 1

def unregister_thread(self, thread):
assert(thread in self.threads and thread.task is self)
Expand Down Expand Up @@ -916,9 +947,12 @@ multiple), giving the thread the chance to handle cancellation promptly so that
self.implicit_thread.resume(Cancelled.TRUE)
else:
assert(self.state == Task.State.STARTED)
candidates = { t for t in self.threads if t.cancellable }
if self.needs_exclusive() and self.inst.exclusive_thread not in { None, self.implicit_thread }:
candidates.discard(self.implicit_thread)
def exclusive_conflict(thread):
return (self.needs_exclusive()
and thread is self.implicit_thread
and (self.inst.exclusive_thread not in { None, self.implicit_thread }
or self.inst.sync_depth > 0))
candidates = { t for t in self.threads if t.cancellable and not exclusive_conflict(t) }
if candidates and self.inst.may_enter_from(caller):
self.state = Task.State.CANCEL_DELIVERED
self.inst.enter_from(caller)
Expand All @@ -932,7 +966,8 @@ thread when doing so would violate [Component Invariant] #2 or #3. In
particular, invariant #2 requires not resuming any thread while the task's
containing component instance may not be reentered and invariant #3 requires not
resuming a `needs_exclusive` task's implicit thread while another task's
implicit thread is running exclusively.
`needs_exclusive` implicit thread is holding the `exclusive_thread` lock *or*
there's a non-`async` call on the stack (which must execute in a LIFO manner).

If cancellation cannot be immediately delivered by `Task.request_cancellation`,
the request is remembered in `Task.state` and delivered at the next opportunity
Expand Down Expand Up @@ -3752,32 +3787,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 `Task.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 task.promotable(t) }
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 +5106,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 `Task.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 +5135,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 `Task.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: 7 additions & 2 deletions design/mvp/Concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,8 @@ 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
also switch to threads that are in a "ready to run" state (when doing so would
not otherwise violate [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
Expand Down Expand Up @@ -711,7 +712,11 @@ the event loop after every event (instead of once at the end of the task),
stackless async exports release the lock between every event, allowing a higher
degree of concurrency than synchronous exports. Stackful async exports ignore
the lock entirely and thus achieve the highest degree of (cooperative)
concurrency.
concurrency. Another source of implicit backpressure arises when, in a [donut
wrapping] scenario, a recursive `async` call into the parent that requires the
exclusive lock is attempted while the parent is actively executing a
non-`async`-typed call (as this would otherwise allow non-LIFO execution that
would break invariant #3).

Since non-`async` functions are not allowed to block (including due to
backpressure) and also don't pile up like `async` functions, non-`async`
Expand Down
20 changes: 11 additions & 9 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2277,8 +2277,9 @@ 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`
thread `t` if `t` is "ready" and doing so would not otherwise violate
[Component Invariant] #3. In any case, the current thread is left in the
"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`.

Expand All @@ -2293,9 +2294,10 @@ 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 `t` if `t` is "ready" and doing so would not otherwise violate
[Component Invariant] #3. In any case, the current thread is left in the "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
Expand Down Expand Up @@ -3011,10 +3013,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 @@ -3342,6 +3343,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
Loading
Loading