-
Notifications
You must be signed in to change notification settings - Fork 97
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
Conversation
- 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
…body awaits (due to do exp {} )
There was a problem hiding this 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.
I don't know if I understand your concern already.
Must use.
|
Co-authored-by: Gabor Greif <gabor@dfinity.org>
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 It's doable, but a bit ugly to both implement and explain. |
@luc-blaeser @ggreif should I implement a type based warning or restriction, or just merge this is as is? |
Leave as-is for now. But we should not advertise this feature as a beginner's solves-all-problems tool. |
Co-authored-by: Gabor Greif <gabor@dfinity.org>
Should I change the Changelog entry? |
|
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:
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 anawait*
-able value.(They cannot be used as the return types of
shared
functions.)Async*
The async expression
async* <block-or-exp>
has typeasync* T
provided:<block-or-exp>
has typeT
;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>
isT
. That is, the return expression,<exp0>
, (implicit or explicit) to any enclosedreturn <exp0>?
expression, must have typeT
.Evaluation of
async* <block-or-exp>
produces a delayed computation to evaluate<block-or-exp>
. It immediately returns a value of typeasync* T
.The delayed computation can be executed using
await*
, producing one evaluationof 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 anawait
.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 anawait*
.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 correspondingawait*
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 eachawait
of its result, which can be undesirable.Await*
The
await*
expressionawait* <exp>
has typeT
provided:<exp>
has typeasync* T
,T
is shared,the
await*
is explicitly enclosed by anasync
-expression or appears in the body of ashared
function.Expression
await <exp>
evaluates<exp>
to a resultr
. Ifr
istrap
, evaluation returnstrap
. Otherwiser
is a delayed computation<block-or-exp>
. The evaluation ofawait* <exp>
proceedswith 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 theawait
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 properawait
.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 properawait
s and may be interleaved with the execution of other, concurrent messages.TODO:
Future:
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 anasync*
return type).That's a drawback compared to the previous
do async/await
approach. But them's the breaks.