Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement async*/await* for efficient async abstraction #3609

Merged
merged 109 commits into from
Dec 7, 2022

Conversation

crusso
Copy link
Contributor

@crusso crusso commented Nov 25, 2022

Add opt-in support for efficient abstraction of asynchronous code using async*/await* to delineate possible commits/context switches. Fixes #1482 and addresses https://forum.dfinity.org/t/canister-output-message-queue-limits-and-ic-management-canister-throttling-limits/15972/22.

Syntax:

<type ::= ...
  async* <typ>                                  delayed, asynchronous computation

<exp> ::= ...
  async* <block-or-exp>                          delay an asynchronous computation
  await* <block-or-exp>                          await a delayed computation (only in async)

This is another take on the design, avoid some of the pitfalls (but also advantages) of #3573 which proposed an eager do async <block_or_exp> but no new types.

Async* types

async* <typ> specifies a delayed, asynchronous computation producing a value of type <typ>.

Computation types typically appear as the result type of a local function that produces an await*-able value.

(They cannot be used as the return types of shared functions.)

Async*

The async expression async* <block-or-exp> has type async* T provided:

  • <block-or-exp> has type T;

  • T is shared.

Any control-flow label in scope for async* <block-or-exp> is not in scope for <block-or-exp>. However, <block-or-exp> may declare and use its own, local, labels.

The implicit return type in <block-or-exp> is T. That is, the return expression, <exp0>, (implicit or explicit) to any enclosed return <exp0>? expression, must have type T.

Evaluation of async* <block-or-exp> produces a delayed computation to evaluate <block-or-exp>. It immediately returns a value of type async* T.
The delayed computation can be executed using await*, producing one evaluation
of the computation <block-or-exp>.

Danger

Note that async <block-or-exp> has the effect of scheduling a single asynchronous computation of <exp>, regardless of whether its result, a future, is consumed with an await.
Moreover, each additional consumption by an await just returns the previous result, without repeating the computation.

In comparison, async* <block-or_exp>, has no effect until its value is consumed by an await*.
Moreover, each additional consumption by an await* will trigger a new evaluation of <block-or-exp>.

Be careful of this distinction, and other differences, when refactoring code.

Note:

The async* and corresponding await* constructs are useful for efficiently abstracting asynchronous code into re-useable functions.
In comparison, calling a local function that returns a proper async type requires committing state and suspending execution with each await of its result, which can be undesirable.

Await*

The await* expression await* <exp> has type T provided:

  • <exp> has type async* T,

  • T is shared,

  • the await* is explicitly enclosed by an async-expression or appears in the body of a shared function.
    Expression await <exp> evaluates <exp> to a result r. If r is trap, evaluation returns trap. Otherwise r is a delayed computation <block-or-exp>. The evaluation of await* <exp> proceeds
    with the evaluation of <block-or-exp>, executing the delayed computation.

Danger

During the evaluation of <block-or-exp>, the state of the enclosing actor may change due to concurrent processing of other incoming actor messages. It is the programmer’s responsibility to guard against non-synchronized state changes.

Note

Unlike await, which, regardless of the dynamic status of the future, ensures that all tentative state changes and message sends prior to the await are committed and irrevocable, await* does not, in itself, commit any state changes, nor does it suspend computation.
Instead, evaluation proceeds immediately according to <block-or-exp> (the value of <exp>), committing state and suspending execution whenever <block-or-exp> does (but not otherwise).

Note

Evaluation of a delayed async* block is synchronous while possible, switching to asynchronous when necessary due to a proper await.

Using await* signals that the computation may commit state and suspend execution during the evaluation of <block-or-exp>, that is, that evaluation of <block-or-exp> may perform zero or more proper awaits and may be interleaved with the execution of other, concurrent messages.

TODO:

Future:

  • generics and flattening - rule out generics for now?
  • relax shared content type for async* types?

One annoying thing is that we cannot make actor class instantiation yet more efficient without returning an async*, breaking code. Although I guess users could opt in to that if wanted (by giving an async* return type).
That's a drawback compared to the previous do async/await approach. But them's the breaks.

- BUG: stateful refunds still can cause illegal ic call for refunds before context switch (see calc.mo)
- adapt await.ml to support non-unit answer types in cps transform
- ugly version of async.ml (needs cleanup to avoid switching codegen on current answer type)
- simple tests
- TODO make effect of do async e be effect of e (not T.Triv as current)
- TODO update design/asynccps.md to match
- test nested async block
src/ir_def/construct.ml Outdated Show resolved Hide resolved
src/ir_def/construct.ml Outdated Show resolved Hide resolved
src/ir_def/construct.ml Outdated Show resolved Hide resolved
Copy link
Contributor

@ggreif ggreif left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I am surprised there is not more duplication, but that is a good thing. A few nits I found are marked.

@iclighthouse
Copy link

I don't know if I understand your concern already.
I think it is possible to disable future references to the async* f() function.
To disable the use of.

let a = f(); 
await* a;

Must use.

let a = await* f();

design/asynccps.md Outdated Show resolved Hide resolved
Co-authored-by: Gabor Greif <gabor@dfinity.org>
src/mo_frontend/typing.ml Outdated Show resolved Hide resolved
@crusso
Copy link
Contributor Author

crusso commented Dec 6, 2022

I don't know if I understand your concern already. I think it is possible to disable future references to the async* f() function. To disable the use of.

let a = f(); 
await* a;

Must use.

let a = await* f();

Yes, that is the general idea.

The problem is that these are just ordinary constructs with special types so you have to rule out the introduction of those types in positions that aren't enclosed by await*.

It's doable, but a bit ugly to both implement and explain.

@crusso crusso closed this Dec 6, 2022
@crusso crusso reopened this Dec 6, 2022
@crusso
Copy link
Contributor Author

crusso commented Dec 6, 2022

@luc-blaeser @ggreif should I implement a type based warning or restriction, or just merge this is as is?

@ggreif
Copy link
Contributor

ggreif commented Dec 6, 2022

Leave as-is for now. But we should not advertise this feature as a beginner's solves-all-problems tool.

Changelog.md Outdated Show resolved Hide resolved
Co-authored-by: Gabor Greif <gabor@dfinity.org>
@crusso
Copy link
Contributor Author

crusso commented Dec 6, 2022

But we should not advertise this feature as a beginner's solves-all-problems tool.

Should I change the Changelog entry?

@crusso crusso added the automerge-squash When ready, merge (using squash) label Dec 6, 2022
@ggreif
Copy link
Contributor

ggreif commented Dec 6, 2022

Should I change the Changelog entry?

This feature is experimental and may evolve in future. is enough, I think.

@mergify mergify bot merged commit 26c229a into master Dec 7, 2022
@mergify mergify bot deleted the claudio/async-star branch December 7, 2022 02:35
@mergify mergify bot removed the automerge-squash When ready, merge (using squash) label Dec 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support direct abstraction of code that awaits into functions, without requiring an unnecessary async
5 participants