@@ -100,26 +100,20 @@ to define their imports and exports.
100
100
101
101
To provide wasm runtimes with additional optimization opportunities for
102
102
languages with "stackless" concurrency (e.g. languages using ` async ` /` await ` ),
103
- two ` async ` ABI sub-options are provided: a "stackless" ABI selected by
104
- providing a ` callback ` function and a "stackful" ABI selected by * not*
105
- providing a ` callback ` function. The stackless ABI allows core wasm to
106
- repeatedly return to an [ event loop] to receive events concerning a selected
107
- set of "waitables", thereby clearing the native stack when waiting for events
108
- and allowing the runtime to reuse stack segments between events. In the
109
- [ future] ( #TODO ) , a ` strict-callback ` option may be added to require (via
110
- runtime traps) * all* waiting to happen via the event loop, thereby giving the
111
- engine more up-front information that the engine can use to avoid allocating
112
- [ fibers] in more cases. In the meantime, to support complex applications with
113
- mixed dependencies and concurrency models, the ` callback ` immediate allows
114
- * both* returning to the event loop * and* making blocking calls to wait for
115
- event.
103
+ two async ABI sub-options are provided: a "stackless" async ABI selected by
104
+ providing a ` callback ` function and a "stackful" async ABI selected by * not*
105
+ providing a ` callback ` function. The stackless async ABI allows core wasm to
106
+ repeatedly return to an event loop to receive events (delivered to the
107
+ ` callback ` ), thereby clearing the native stack for the benefit of the wasm
108
+ runtime while waiting in the event loop.
116
109
117
110
To propagate backpressure, it's necessary for a component to be able to say
118
- "there are too many async export calls already in progress, don't start any
119
- more until I let some of them complete". Thus, the low-level async ABI provides
120
- a way to apply and release backpressure.
111
+ "there are too many concurrent export calls already in progress, don't start
112
+ any more until I let some of them complete". Thus, the Component Model provides
113
+ a built-in way for a component instance to apply and release backpressure that
114
+ callers must always be prepared to handle.
121
115
122
- With this backpressure protocol in place, there is a natural way for sync and
116
+ With this backpressure mechanism in place, there is a natural way for sync and
123
117
async code to interoperate:
124
118
1 . If an async component calls a sync component and the sync component blocks,
125
119
execution is immediately returned to the async component, effectively
@@ -291,20 +285,22 @@ in the Canonical ABI explainer.
291
285
292
286
### Structured concurrency
293
287
294
- Calling * into* a component creates a ` Task ` to track ABI state related to the
295
- * callee* (like "number of outstanding borrows").
296
-
297
- Calling * out* of a component creates a ` Subtask ` to track ABI state related to
298
- the * caller* (like "which handles have been lent").
299
-
300
- When one component calls another, there is thus a ` Subtask ` +` Task ` pair that
301
- collectively maintains the overall state of the call and enforces that both
302
- components uphold their end of the ABI contract. But when the host calls into
303
- a component, there is only a ` Task ` and, symmetrically, when a component calls
304
- into the host, there is only a ` Subtask ` .
305
-
306
- Based on this, the call stack when a component calls a host-defined import will
307
- have the general form:
288
+ As mentioned above, calling a component export creates a task to track the
289
+ state used to enforce Canonical ABI rules that apply to the callee (an example
290
+ being: the number of received borrowed handles that still need to be dropped
291
+ before the call returns).
292
+
293
+ Symmetrically, calling a component * import* creates a ** subtask** to track the
294
+ state used to enforce Canonical ABI rules that apply to the * caller* (an
295
+ example being: which handles have been lent that the caller can't drop until
296
+ the call resolves).
297
+
298
+ When one component calls another, there is thus a new task+subtask pair created
299
+ to ensure that both components uphold their end of the Canonical ABI rules. But
300
+ when the host calls a component export, there is only a task and,
301
+ symmetrically, when a component calls a host-defined import, there is only a
302
+ subtask. Thus, the ** async call stack** at the point when a component calls a
303
+ host-defined import will have the general form:
308
304
```
309
305
[Host]
310
306
↓ host calls component export
@@ -314,27 +310,28 @@ have the general form:
314
310
↓ component calls import implemented by the host
315
311
[Component Subtask <> Host task]
316
312
```
317
- Here, the ` <- ` arrow represents the ` supertask ` relationship that is immutably
318
- established when first making the call. A paired ` Subtask ` and ` Task ` have the
319
- same ` supertask ` and can thus be visualized as a single node in the callstack.
320
-
321
- (These concepts are represented in the Canonical ABI Python code via the
322
- [ ` Task ` ] and [ ` Subtask ` ] classes.)
323
-
324
- One semantically-observable use of this async call stack is to distinguish
325
- between hazardous ** recursive reentrance** , in which a component instance is
326
- reentered when one of its tasks is already on the callstack, from
327
- business-as-usual ** sibling reentrance** , in which a component instance is
328
- freshly reentered when its other tasks are suspended waiting on I/O. Recursive
329
- reentrance currently always traps, but may be allowed (and indicated to core
330
- wasm) in an opt-in manner in the [ future] ( #TODO ) .
313
+ Here, the ` ↓ ` arrow represents the ** subtask** relationship (the dual of which
314
+ is the ** supertask** relationship). Since a task+subtask pair have the same
315
+ supertask, they can be thought of as a single node in the async call stack.
316
+
317
+ A subtask/supertask relationship is immutably established when an import is
318
+ called, setting the [ current task] ( #current-task ) as the supertask
319
+ of the new subtask created for the import call.
320
+
321
+ A semantically-observable use of the async call stack is to distinguish between
322
+ hazardous ** recursive reentrance** , in which a component instance is reentered
323
+ when one of its tasks is already on the callstack, from business-as-usual
324
+ ** sibling reentrance** , in which a component instance is reentered for the
325
+ first time on a particular async call stack. Recursive reentrance currently
326
+ always traps, but will be allowed (and indicated to core wasm) in an opt-in
327
+ manner in the [ future] ( #TODO ) .
331
328
332
329
The async call stack is also useful for non-semantic purposes such as providing
333
- backtraces when debugging, profiling and distributed tracing. While particular
334
- languages can and do maintain their own async call stacks in core wasm state,
335
- without the Component Model's async call stack, linkage * between* different
336
- languages would be lost at component boundaries, leading to a loss of overall
337
- context in multi-component applications.
330
+ backtraces when debugging, profiling and tracing. While particular languages
331
+ can and do maintain their own async call stacks in core wasm state, without the
332
+ Component Model's async call stack, linkage * between* different languages would
333
+ be lost at component boundaries, leading to a loss of overall context in
334
+ multi-component applications.
338
335
339
336
There is an important nuance to the Component Model's minimal form of
340
337
Structured Concurrency compared to Structured Concurrency support that appears
@@ -464,7 +461,7 @@ the Canonical-ABI-defined "yield" code to the event loop.
464
461
465
462
### Backpressure
466
463
467
- Once a component exports asynchronously-lifted functions, multiple concurrent
464
+ Once a component exports functions using the async ABI , multiple concurrent
468
465
export calls can start piling up, each consuming some of the component's finite
469
466
private resources (like linear memory), requiring the component to be able to
470
467
exert * backpressure* to allow some tasks to finish (and release private
@@ -473,26 +470,23 @@ call the [`backpressure.set`] built-in to set a component-instance-wide
473
470
"backpressure" flag that causes subsequent export calls to immediately return
474
471
in the "starting" state without calling the component's Core WebAssembly code.
475
472
476
- See the [ ` canon_backpressure_set ` ] function and [ ` Task.enter ` ] method in the
477
- Canonical ABI explainer for the setting and implementation of backpressure.
478
-
479
473
In addition to * explicit* backpressure set by wasm code, there is also an
480
- * implicit* source of backpressure used to protect non-reentrant core wasm
481
- code. In particular, when an export is lifted synchronously or using an
482
- ` async callback ` , a component-instance-wide lock is implicitly acquired every
483
- time core wasm is executed. By returning to the event loop after every event
484
- (instead of once at the end of the task), ` async callback ` exports release
485
- the lock between every event, allowing a higher degree of concurrency than
486
- synchronous exports. ` async ` (stackful) exports ignore the lock entirely and
487
- thus achieve the highest degree of (cooperative) concurrency.
474
+ * implicit* source of backpressure used to protect non-reentrant core wasm code.
475
+ In particular, when an export uses the sync ABI or the stackless async ABI, a
476
+ component-instance-wide lock is implicitly acquired every time core wasm is
477
+ executed. By returning to the event loop after every event (instead of once at
478
+ the end of the task), stackless async exports release the lock between every
479
+ event, allowing a higher degree of concurrency than synchronous exports.
480
+ Stackfull async exports ignore the lock entirely and thus achieve the highest
481
+ degree of (cooperative) concurrency.
488
482
489
483
Once a task is allowed to start according to these backpressure rules, its
490
484
arguments are lowered into the callee's linear memory and the task is in
491
485
the "started" state.
492
486
493
487
### Returning
494
488
495
- The way an async function returns its value is by calling [ ` task.return ` ] ,
489
+ The way an async export call returns its value is by calling [ ` task.return ` ] ,
496
490
passing the core values that are to be lifted as * parameters* .
497
491
498
492
Returning values by calling ` task.return ` allows a task to continue executing
@@ -507,9 +501,7 @@ loop interleaving `stream.read`s (of the readable end passed for `in`) and
507
501
` stream.write ` s (of the writable end it ` stream.new ` ed) before exiting the
508
502
task.
509
503
510
- Once ` task.return ` is called, the task is in the "returned" state and can
511
- finish execution any time thereafter. See the [ ` canon_task_return ` ] function in
512
- the Canonical ABI explainer for more details.
504
+ Once ` task.return ` is called, the task is in the "returned" state.
513
505
514
506
### Borrows
515
507
@@ -527,10 +519,10 @@ callee task that is decremented when the `borrow` handle is dropped. If a
527
519
callee task attempts to return when its ` num_borrows ` is greater than zero, the
528
520
callee traps.
529
521
530
- In an asynchronous setting, the only generalization necessary is that, since
531
- there can be multiple overlapping async tasks executing in a component
532
- instance, a borrowed handle must track * which * task's ` num_borrow ` s was
533
- incremented so that the correct counter can be decremented .
522
+ In an asynchronous setting, since there can be multiple overlapping async tasks
523
+ executing in a component instance, a borrowed handle must track * which * task's
524
+ ` num_borrow ` s was incremented so that the correct counter can be decremented
525
+ and there is a trap upon ` task.return ` if ` num_borrows ` is nonzero .
534
526
535
527
### Cancellation
536
528
@@ -584,28 +576,28 @@ Canonical ABI explainer for more details.
584
576
585
577
### Nondeterminism
586
578
587
- Given the general goal of supporting concurrency, Component Model async
588
- necessarily introduces a degree of nondeterminism. Async concurrency is however
589
- [ cooperative] , meaning that nondeterministic behavior can only be observed at
590
- well-defined points in the program. This contrasts with non-cooperative
591
- [ multithreading] in which nondeterminism can be observed at every core wasm
592
- instruction.
593
-
594
- One inherent source of potential nondeterminism that is independent of async is
595
- the behavior of host-defined import and export calls. Async extends this
596
- host-dependent nondeterminism to the behavior of the ` read ` and ` write `
597
- built-ins called on ` stream ` s and ` future ` s that have been passed to and from
598
- the host via host-defined import and export calls. However, just as with import
599
- and export calls, it is possible for a host to define a deterministic ordering
600
- of ` stream ` and ` future ` ` read ` and ` write ` behavior such that overall
601
- component execution is deterministic.
579
+ Component Model concurrency support necessarily introduces a degree of
580
+ nondeterminism. However, until Core WebAssembly adds
581
+ [ shared-everything-threads] , Component Model concurrency is [ cooperative] ,
582
+ which means that nondeterministic behavior can only be observed at well-defined
583
+ points in the program. Once [ shared-everything-threads] is added,
584
+ WebAssembly's full [ weak memory model] will be observable, but only within
585
+ components that use the new ` shared ` attribute on functions.
586
+
587
+ One inherent source of potential nondeterminism that is independent of the
588
+ Component Model is the behavior of host-defined import and export calls.
589
+ Component Model concurrency extends this host-dependent nondeterminism to the
590
+ behavior of the ` read ` and ` write ` built-ins called on ` stream ` s and ` future ` s
591
+ that have been passed to and from the host. However, just as with import and
592
+ export calls, it is possible for a host to define a deterministic ordering of
593
+ ` stream ` and ` future ` ` read ` and ` write ` behavior such that overall component
594
+ execution is deterministic.
602
595
603
596
In addition to the inherent host-dependent nondeterminism, the Component Model
604
597
adds several internal sources of nondeterministic behavior that are described
605
598
next. However, each of these sources of nondeterminism can be removed by a host
606
- implementing the WebAssembly [ Determinsic Profile] , maintaining the ability for
607
- a host to provide spec-defined deterministic component execution for components
608
- even when they use async.
599
+ implementing the WebAssembly [ Deterministic Profile] , maintaining the ability for
600
+ a host to provide spec-defined deterministic component execution for components.
609
601
610
602
The following sources of nondeterminism arise via internal built-in operations
611
603
defined by the Component Model:
0 commit comments