Skip to content

Commit f858bd3

Browse files
authored
Avoid blocking when waitables are already ready and document non-determinism (#514)
1 parent cdbf5cc commit f858bd3

File tree

3 files changed

+82
-10
lines changed

3 files changed

+82
-10
lines changed

design/mvp/Async.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ summary of the motivation and animated sketch of the design in action.
2323
* [Returning](#returning)
2424
* [Borrows](#borrows)
2525
* [Cancellation](#cancellation)
26+
* [Nondeterminism](#nondeterminism)
2627
* [Async ABI](#async-abi)
2728
* [Async Import ABI](#async-import-abi)
2829
* [Async Export ABI](#async-export-abi)
@@ -550,6 +551,60 @@ a callee can continue executing before exiting the task.
550551
See the [`canon_subtask_cancel`] and [`canon_task_cancel`] functions in the
551552
Canonical ABI explainer for more details.
552553

554+
### Nondeterminism
555+
556+
Given the general goal of supporting concurrency, Component Model async
557+
necessarily introduces a degree of nondeterminism. Async concurrency is however
558+
[cooperative], meaning that nondeterministic behavior can only be observed at
559+
well-defined points in the program. This contrasts with non-cooperative
560+
[multithreading] in which nondeterminism can be observed at every core wasm
561+
instruction.
562+
563+
One inherent source of potential nondeterminism that is independent of async is
564+
the behavior of host-defined import and export calls. Async extends this
565+
host-dependent nondeterminism to the behavior of the `read` and `write`
566+
built-ins called on `stream`s and `future`s that have been passed to and from
567+
the host via host-defined import and export calls. However, just as with import
568+
and export calls, it is possible for a host to define a deterministic ordering
569+
of `stream` and `future` `read` and `write` behavior such that overall
570+
component execution is deterministic.
571+
572+
In addition to the inherent host-dependent nondeterminism, the Component Model
573+
adds several internal sources of nondeterministic behavior that are described
574+
next. However, each of these sources of nondeterminism can be removed by a host
575+
implementing the WebAssembly [Determinsic Profile], maintaining the ability for
576+
a host to provide spec-defined deterministic component execution for components
577+
even when they use async.
578+
579+
The following sources of nondeterminism arise via internal built-in operations
580+
defined by the Component Model:
581+
* If there are multiple waitables with a pending event in a waitable set that
582+
is being waited on or polled, there is a nondeterministic choice of which
583+
waitable's event is delivered first.
584+
* If multiple tasks wait on or poll the same waitable set at the same time,
585+
the distribution of events to tasks is nondeterministic.
586+
* If multiple tasks that previously blocked are unblocked at the same time, the
587+
sequential order in which they are executed is nondeterministic.
588+
* Whenever a task yields or waits on (or polls) a waitable set with an already
589+
pending event, whether the task "blocks" and transfers execution to its async
590+
caller is nondeterministic.
591+
592+
Despite the above, the following scenarios do behave deterministically:
593+
* If a component `a` asynchronously calls the export of another component `b`,
594+
control flow deterministically transfers to `b` and then back to `a` when
595+
`b` returns or blocks.
596+
* If a component `a` asynchronously cancels a subtask in another component `b`,
597+
control flow deterministically transfers to `b` and then back to `a` when `b`
598+
resolves or blocks.
599+
* If a component `a` asynchronously cancels a subtask in another component `b`
600+
that was blocked before starting due to backpressure, cancellation completes
601+
deterministically and immediately.
602+
* When both ends of a stream or future are owned by wasm components, the
603+
behavior of all read, write, cancel and close operations is deterministic
604+
(modulo any nondeterminitic execution that determines the ordering in which
605+
the operations are performed).
606+
607+
553608
## Async ABI
554609

555610
At an ABI level, native async in the Component Model defines for every WIT
@@ -1001,6 +1056,8 @@ comes after:
10011056
[Unit]: https://en.wikipedia.org/wiki/Unit_type
10021057
[Thread-local Storage]: https://en.wikipedia.org/wiki/Thread-local_storage
10031058
[FS or GS Segment Base Address]: https://docs.kernel.org/arch/x86/x86_64/fsgs.html
1059+
[Cooperative]: https://en.wikipedia.org/wiki/Cooperative_multitasking
1060+
[Multithreading]: https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)
10041061

10051062
[AST Explainer]: Explainer.md
10061063
[Lift and Lower Definitions]: Explainer.md#canonical-definitions
@@ -1047,6 +1104,9 @@ comes after:
10471104
[Reentrance]: Explainer.md#component-invariants
10481105
[`start`]: Explainer.md#start-definitions
10491106

1107+
[Store]: https://webassembly.github.io/spec/core/exec/runtime.html#syntax-store
1108+
[Deterministic Profile]: https://webassembly.github.io/spec/versions/core/WebAssembly-3.0-draft.pdf#subsubsection*.798
1109+
10501110
[stack-switching]: https://github.com/WebAssembly/stack-switching/
10511111
[JSPI]: https://github.com/WebAssembly/js-promise-integration/
10521112
[shared-everything-threads]: https://github.com/webAssembly/shared-everything-threads

design/mvp/CanonicalABI.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -822,12 +822,15 @@ Python [awaitable] using the `OnBlock` callback described above:
822822
self.maybe_start_pending_task()
823823

824824
awaitable = asyncio.ensure_future(awaitable)
825-
cancelled = await self.on_block(awaitable)
826-
if cancelled and not cancellable:
827-
assert(self.state == Task.State.INITIAL)
828-
self.state = Task.State.PENDING_CANCEL
825+
if awaitable.done() and not DETERMINISTIC_PROFILE and random.randint(0,1):
826+
cancelled = False
827+
else:
829828
cancelled = await self.on_block(awaitable)
830-
assert(not cancelled)
829+
if cancelled and not cancellable:
830+
assert(self.state == Task.State.INITIAL)
831+
self.state = Task.State.PENDING_CANCEL
832+
cancelled = await self.on_block(awaitable)
833+
assert(not cancelled)
831834

832835
if sync:
833836
self.inst.calling_sync_import = False
@@ -838,6 +841,12 @@ Python [awaitable] using the `OnBlock` callback described above:
838841

839842
return cancelled
840843
```
844+
If the given `awaitable` is already resolved (e.g., if between making an async
845+
import call that blocked and calling `waitable-set.wait` the I/O operation
846+
completed), the Component Model allows the runtime to nondeterministically
847+
avoid calling `OnBlock` which, in component-to-component async calls, means
848+
that control flow does not need to transfer to the calling component.
849+
841850
If `wait_on` is called with `sync` set to `True`, only tasks in *other*
842851
component instances may execute; no code in the current component instance may
843852
execute. This is achieved by setting and waiting on `calling_sync_import`

design/mvp/canonical-abi/definitions.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,12 +531,15 @@ async def wait_on(self, awaitable, sync, cancellable = False) -> bool:
531531
self.maybe_start_pending_task()
532532

533533
awaitable = asyncio.ensure_future(awaitable)
534-
cancelled = await self.on_block(awaitable)
535-
if cancelled and not cancellable:
536-
assert(self.state == Task.State.INITIAL)
537-
self.state = Task.State.PENDING_CANCEL
534+
if awaitable.done() and not DETERMINISTIC_PROFILE and random.randint(0,1):
535+
cancelled = False
536+
else:
538537
cancelled = await self.on_block(awaitable)
539-
assert(not cancelled)
538+
if cancelled and not cancellable:
539+
assert(self.state == Task.State.INITIAL)
540+
self.state = Task.State.PENDING_CANCEL
541+
cancelled = await self.on_block(awaitable)
542+
assert(not cancelled)
540543

541544
if sync:
542545
self.inst.calling_sync_import = False

0 commit comments

Comments
 (0)