|
| 1 | +# Behavior of `yield`/`yield*` in `async*` functions and `await for` loops. |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +**Version**: 1.0 (2018-11-30) |
| 6 | + |
| 7 | +## Background |
| 8 | +See [Issue #121](http://github.com/dart-lang/language/issues/121). |
| 9 | + |
| 10 | +The Dart language specification defines the behavior of `async*` functions, |
| 11 | +and `yield` and `yield*` statements in those, as well as the behavior of |
| 12 | +`await for` loops. |
| 13 | + |
| 14 | +This specification has not always been precise, and the implemented behavior |
| 15 | +has been causing user problems ([34775](https://github.com/dart-lang/sdk/issues/34775), |
| 16 | +[22351](https://github.com/dart-lang/sdk/issues/22351), |
| 17 | +[35063](https://github.com/dart-lang/sdk/issues/35063), |
| 18 | +[25748](https://github.com/dart-lang/sdk/issues/25748)). |
| 19 | + |
| 20 | +The specification was cleaned up prior to the Dart 2 released, |
| 21 | +but implementations have not been unified and do not match the documented |
| 22 | +behavior. |
| 23 | + |
| 24 | +The goal is that users can predict when an `async*` function will block |
| 25 | +at a `yield`, and have it interact seamlessly with `await for` consumption |
| 26 | +of the created stream. |
| 27 | +Assume that an `await for` loop is iterating over a stream created by an |
| 28 | +`async*` function. |
| 29 | +The `await for` loop pauses its stream subscription whenever its body does |
| 30 | +something asynchronous. That pause should block the `async*` function at |
| 31 | +the `yield` statement producing the event that caused the `await for` |
| 32 | +loop to enter its body. When the `await for` loop finishes its body, |
| 33 | +and resumes the subscription, only then may the `async*` function start |
| 34 | +executing code after the `yield`. |
| 35 | +If the `await for` loop cancels the iteration (breaking out of the loop in any |
| 36 | +way) then the subscription is canceled, |
| 37 | +and then the `async*` function should return at the `yield` statement |
| 38 | +that produced the event that caused the await loop to enter its body. |
| 39 | +The `await for` loop will wait for the cancellation future (the one |
| 40 | +returned by `StreamSubscription.cancel`) which may capture any errors |
| 41 | +that the `async*` function throws while returning |
| 42 | +(typically in `finally` blocks). |
| 43 | + |
| 44 | +That is: Execution of an `async*` function producing a stream, |
| 45 | +and an `await for` loop consuming that stream, must occur in *lock step*. |
| 46 | + |
| 47 | +## Feature Specification |
| 48 | + |
| 49 | +The language specification already contains a formal specification of the |
| 50 | +behavior. |
| 51 | +The following is a non-normative description of the specified behavior. |
| 52 | + |
| 53 | +An `async*` function returns a stream. |
| 54 | +Listening on that stream executes the function body linked to the |
| 55 | +stream subscription returned by that `listen` call. |
| 56 | + |
| 57 | +A `yield e;` statement is specified such that it must successfully *deliver* |
| 58 | +the event to the subscription before continuing. |
| 59 | +If the subscription is canceled, delivering succeeds and does nothing, |
| 60 | +if the subscription is a paused, |
| 61 | +delivery succeeds when the event is accepted and buffered. |
| 62 | +Otherwise, deliver is successful after the subscription's event listener |
| 63 | +has been invoked with the event object. |
| 64 | + |
| 65 | +After this has happened, the subscription is checked for being |
| 66 | +canceled or paused. |
| 67 | +If paused, the function is blocked at the `yield` statement until |
| 68 | +the subscription is resumed or canceled. |
| 69 | +In this case the `yield` is an asynchronous operation (it does not complete |
| 70 | +synchronously, but waits for an external event, the resume, |
| 71 | +before it continues). |
| 72 | +If canceled, including if the cancel happens during a pause, |
| 73 | +the `yield` statement acts like a `return;` statement. |
| 74 | + |
| 75 | +A `yield* e;` statement listens on the stream that `e` evaluates to |
| 76 | +and forwards all events to this function's subscription. |
| 77 | +If the subscription is paused, the pause is forwarded to the yielded stream |
| 78 | +If the subscription is canceled, the cancel is forwarded to the yielded stream, |
| 79 | +then the `yield*` statement waits for any cancellation future, and finally |
| 80 | +it acts like a `return;` statement. |
| 81 | +If the yielded stream completes, the yield statement completes normally. |
| 82 | +A `yield*` is *always* an asynchronous operation. |
| 83 | + |
| 84 | +In an asynchronous function, an `await for (var event in stream) ...` loop |
| 85 | +first listens on the iterated stream, then for each data event, it executes the |
| 86 | +body. If the body performs any asynchronous operation (that is, |
| 87 | +it does not complete synchronously because it executes any `await`, |
| 88 | +`wait for` or `yield*` operation, or it blocks at a `yield`), then |
| 89 | +the stream subscription must be paused. It is resumed again when the |
| 90 | +body completes normally. If the loop body breaks the loop (by any means, |
| 91 | +including throwing or breaking), or if the iterated stream produces an error, |
| 92 | +then the loop is broken. Then the subscription is canceled and the cancellation |
| 93 | +future is awaited, and then the loop completes in the same way as the body |
| 94 | +or by throwing the produced error and its accompanying stack trace. |
| 95 | + |
| 96 | +Notice that there is no requirement that an `async*` implementation must call |
| 97 | +the subscription event handler *synchronously*, but if not, then it must |
| 98 | +block at the `yield` until the event has been delivered. Since it's possible |
| 99 | +to deliver the event synchronously, it's likely that that will be the |
| 100 | +implementation, and it's possible that performance may improve due to this. |
| 101 | + |
| 102 | +### Consequences |
| 103 | +Implementations currently do not block at a `yield` when the delivery of |
| 104 | +the event causes a pause. They simply does not allow a `yield` statement |
| 105 | +to act asynchronously. They can *cancel* at a yield if the cancel happened |
| 106 | +prior to the `yield`, and can easily be made to respect a cancel happening |
| 107 | +during the `yield` event delivery, but they only check for pause *before* |
| 108 | +delivering the event, and it requires a rewrite of the Kernel transformer |
| 109 | +to change this behavior. |
| 110 | + |
| 111 | +### Example |
| 112 | +```dart |
| 113 | +Stream<int> computedStream(int n) async* { |
| 114 | + for (int i = 0; i < n; i++) { |
| 115 | + var value = expensiveComputation(i); |
| 116 | + yield value; |
| 117 | + } |
| 118 | +} |
| 119 | +
|
| 120 | +Future<void> consumeValues(Stream<int> values, List<int> log) async { |
| 121 | + await for (var value in values) { |
| 122 | + if (value < 0) break; |
| 123 | + if (value > 100) { |
| 124 | + var newValue = await complexReduction(value); |
| 125 | + if (newValue < 0) break; |
| 126 | + log.add(newValue); |
| 127 | + } else { |
| 128 | + log.add(value); |
| 129 | + } |
| 130 | + } |
| 131 | +} |
| 132 | +
|
| 133 | +void main() async { |
| 134 | + var log = <int>[]; |
| 135 | + await consumeValues(computedStream(25), log); |
| 136 | + print(log); |
| 137 | +} |
| 138 | +``` |
| 139 | +In this example, the `await for` in the `consumeValues` function should get a chance to abort or pause |
| 140 | +(by doing something asynchronous) its stream subscription *before* the next `expensiveComputation` starts. |
| 141 | + |
| 142 | +The current implementation starts the next expensive operation before it checks whether it should |
| 143 | +abort. |
0 commit comments