From d3672493cc6ad5085f202df1859b13b4ae4dec96 Mon Sep 17 00:00:00 2001
From: Andrew Kelley
+ When a function is called, a frame is pushed to the stack,
+ the function runs until it reaches a return statement, and then the frame is popped from the stack.
+ At the callsite, the following code does not run until the function returns.
+
An async function is a function whose callsite is split into an {#syntax#}async{#endsyntax#} initiation,
- followed by an {#syntax#}await{#endsyntax#} completion.
+ followed by an {#syntax#}await{#endsyntax#} completion. Its frame is
+ provided explicitly by the caller, and it can be suspended and resumed any number of times.
- When you call a function, it creates a stack frame,
- and then the function runs until it reaches a return
- statement, and then the stack frame is destroyed.
- At the callsite, the next line of code does not run
- until the function returns.
+ Zig infers that a function is {#syntax#}async{#endsyntax#} when it observes that the function contains
+ a suspension point. Async functions can be called the same as normal functions. A
+ function call of an async function is a suspend point.
- An async function is like a function, but it can be suspended
- and resumed any number of times, and then it must be
- explicitly destroyed. When an async function suspends, it
- returns to the resumer.
-
- Declare an async function with the {#syntax#}async{#endsyntax#} keyword.
- The expression in angle brackets must evaluate to a struct
- which has these fields:
-
- You may notice that this corresponds to the {#syntax#}std.mem.Allocator{#endsyntax#} interface.
- This makes it convenient to integrate with existing allocators. Note, however,
- that the language feature does not depend on the standard library, and any struct which
- has these fields is allowed.
-
- Omitting the angle bracket expression when defining an async function makes
- the function generic. Zig will infer the allocator type when the async function is called.
-
- Call an async function with the {#syntax#}async{#endsyntax#} keyword. Here, the expression in angle brackets
- is a pointer to the allocator struct that the async function expects.
-
- The result of an async function call is a {#syntax#}promise->T{#endsyntax#} type, where {#syntax#}T{#endsyntax#}
- is the return type of the async function. Once a promise has been created, it must be
- consumed with {#syntax#}await{#endsyntax#}:
-
- Async functions start executing when created, so in the following example, the entire
- TODO
+ At any point, a function may suspend itself. This causes control flow to
+ return to the callsite (in the case of the first suspension),
+ or resumer (in the case of subsequent suspensions).
- At any point, an async function may suspend itself. This causes control flow to
- return to the caller or resumer. The following code demonstrates where control flow
- goes:
-
- TODO another test example here
-
- When an async function suspends itself, it must be sure that it will be
- resumed somehow, for example by registering its promise handle
- in an event loop. Use a suspend capture block to gain access to the
- promise (TODO this is outdated):
+ In the same way that each allocation should have a corresponding free,
+ Each {#syntax#}suspend{#endsyntax#} should have a corresponding {#syntax#}resume{#endsyntax#}.
+ A suspend block allows a function to put a pointer to its own
+ frame somewhere, for example into an event loop, even if that action will perform a
+ {#syntax#}resume{#endsyntax#} operation on a different thread.
+ {#link|@frame#} provides access to the async function frame pointer.
- Every suspend point in an async function represents a point at which the async function
- could be destroyed. If that happens, {#syntax#}defer{#endsyntax#} expressions that are in
- scope are run, as well as {#syntax#}errdefer{#endsyntax#} expressions.
-
- {#link|Await#} counts as a suspend point.
+ {#syntax#}suspend{#endsyntax#} causes a function to be {#syntax#}async{#endsyntax#}.
Upon entering a {#syntax#}suspend{#endsyntax#} block, the async function is already considered
suspended, and can be resumed. For example, if you started another kernel thread,
- and had that thread call {#syntax#}resume{#endsyntax#} on the promise handle provided by the
- {#syntax#}suspend{#endsyntax#} block, the new thread would begin executing after the suspend
+ and had that thread call {#syntax#}resume{#endsyntax#} on the frame pointer provided by the
+ {#link|@frame#}, the new thread would begin executing after the suspend
block, while the old thread continued executing the suspend block.
@@ -6103,7 +6063,7 @@ test "resume from suspend" {
_ = async testResumeFromSuspend(&my_result);
std.debug.assert(my_result == 2);
}
-async fn testResumeFromSuspend(my_result: *i32) void {
+fn testResumeFromSuspend(my_result: *i32) void {
suspend {
resume @frame();
}
@@ -6113,32 +6073,59 @@ async fn testResumeFromSuspend(my_result: *i32) void {
}
{#code_end#}
- This is guaranteed to be a tail call, and therefore will not cause a new stack frame.
+ This is guaranteed to tail call, and therefore will not cause a new stack frame.
+ In the same way that every {#syntax#}suspend{#endsyntax#} has a matching
+ {#syntax#}resume{#endsyntax#}, every {#syntax#}async{#endsyntax#} has a matching {#syntax#}await{#endsyntax#}.
+
The {#syntax#}await{#endsyntax#} keyword is used to coordinate with an async function's
- {#syntax#}return{#endsyntax#} statement.
+ {#syntax#}return{#endsyntax#} statement.
- {#syntax#}await{#endsyntax#} is valid only in an {#syntax#}async{#endsyntax#} function, and it takes
- as an operand a promise handle.
- If the async function associated with the promise handle has already returned,
- then {#syntax#}await{#endsyntax#} destroys the target async function, and gives the return value.
- Otherwise, {#syntax#}await{#endsyntax#} suspends the current async function, registering its
- promise handle with the target async function. It becomes the target async function's responsibility
- to have ensured that it will be resumed or destroyed. When the target async function reaches
- its return statement, it gives the return value to the awaiter, destroys itself, and then
- resumes the awaiter.
+ {#syntax#}await{#endsyntax#} is a suspend point, and takes as an operand anything that
+ implicitly casts to {#syntax#}anyframe->T{#endsyntax#}.
- A frame handle must be consumed exactly once after it is created with {#syntax#}await{#endsyntax#}.
-
- {#syntax#}await{#endsyntax#} counts as a suspend point, and therefore at every {#syntax#}await{#endsyntax#},
- a async function can be potentially destroyed, which would run {#syntax#}defer{#endsyntax#} and {#syntax#}errdefer{#endsyntax#} expressions.
+ There is a common misconception that {#syntax#}await{#endsyntax#} resumes the target function.
+ It is the other way around: it suspends until the target function completes.
+ In the event that the target function has already completed, {#syntax#}await{#endsyntax#}
+ does not suspend; instead it copies the
+ return value directly from the target function's frame.
In general, {#syntax#}suspend{#endsyntax#} is lower level than {#syntax#}await{#endsyntax#}. Most application
- code will use only {#syntax#}async{#endsyntax#} and {#syntax#}await{#endsyntax#}, but event loop
- implementations will make use of {#syntax#}suspend{#endsyntax#} internally.
+ code will use only {#syntax#}async{#endsyntax#} and {#syntax#}await{#endsyntax#}, but event loop
+ implementations will make use of {#syntax#}suspend{#endsyntax#} internally.
- There are a few issues with async function that are considered unresolved. Best be aware of them,
- as the situation is likely to change before 1.0.0:
+ Putting all of this together, here is an example of typical
+ {#syntax#}async{#endsyntax#}/{#syntax#}await{#endsyntax#} usage:
+
+ Now we remove the {#syntax#}suspend{#endsyntax#} and {#syntax#}resume{#endsyntax#} code, and
+ observe the same behavior, with one tiny difference:
+
+ Previously, the {#syntax#}fetchUrl{#endsyntax#} and {#syntax#}readFile{#endsyntax#} functions suspended,
+ and were resumed in an order determined by the {#syntax#}main{#endsyntax#} function. Now,
+ since there are no suspend points, the order of the printed "... returning" messages
+ is determined by the order of {#syntax#}async{#endsyntax#} callsites.
-
-
-
{#header_close#}
{#header_close#}
@@ -6265,6 +6377,49 @@ comptime {
Note: This function is deprecated. Use {#link|@typeInfo#} instead.
{#syntax#}@asyncCall(frame_buffer: []u8, result_ptr, function_ptr, args: ...) anyframe->T{#endsyntax#}
+ + {#syntax#}@asyncCall{#endsyntax#} performs an {#syntax#}async{#endsyntax#} call on a function pointer, + which may or may not be an {#link|async function|Async Functions#}. +
++ The provided {#syntax#}frame_buffer{#endsyntax#} must be large enough to fit the entire function frame. + This size can be determined with {#link|@frameSize#}. To provide a too-small buffer + invokes safety-checked {#link|Undefined Behavior#}. +
++ {#syntax#}result_ptr{#endsyntax#} is optional ({#link|null#} may be provided). If provided, + the function call will write its result directly to the result pointer, which will be available to + read after {#link|await|Async and Await#} completes. Any result location provided to + {#syntax#}await{#endsyntax#} will copy the result from {#syntax#}result_ptr{#endsyntax#}. +
+ {#code_begin|test#} +const std = @import("std"); +const assert = std.debug.assert; + +test "async fn pointer in a struct field" { + var data: i32 = 1; + const Foo = struct { + bar: async fn (*i32) void, + }; + var foo = Foo{ .bar = func }; + var bytes: [64]u8 = undefined; + const f = @asyncCall(&bytes, {}, foo.bar, &data); + assert(data == 2); + resume f; + assert(data == 4); +} + +async fn func(y: *i32) void { + defer y.* += 2; + y.* += 1; + suspend; +} + {#code_end#} + {#header_close#} + {#header_open|@atomicLoad#}{#syntax#}@atomicLoad(comptime T: type, ptr: *const T, comptime ordering: builtin.AtomicOrder) T{#endsyntax#}
@@ -6855,6 +7010,44 @@ export fn @"A function name that is a complete sentence."() void {} {#see_also|@intToFloat#} {#header_close#} + {#header_open|@frame#} +
{#syntax#}@frame() *@Frame(func){#endsyntax#}
+ + This function returns a pointer to the frame for a given function. This type + can be {#link|implicitly cast|Implicit Casts#} to {#syntax#}anyframe->T{#endsyntax#} and + to {#syntax#}anyframe{#endsyntax#}, where {#syntax#}T{#endsyntax#} is the return type + of the function in scope. +
++ This function does not mark a suspension point, but it does cause the function in scope + to become an {#link|async function|Async Functions#}. +
+ {#header_close#} + + {#header_open|@Frame#} +{#syntax#}@Frame(func: var) type{#endsyntax#}
+ + This function returns the frame type of a function. This works for {#link|Async Functions#} + as well as any function without a specific calling convention. +
++ This type is suitable to be used as the return type of {#link|async|Async and Await#} which + allows one to, for example, heap-allocate an async function frame: +
+ {#code_begin|test#} +const std = @import("std"); + +test "heap allocated frame" { + const frame = try std.heap.direct_allocator.create(@Frame(func)); + frame.* = async func(); +} + +fn func() void { + suspend; +} + {#code_end#} + {#header_close#} + {#header_open|@frameAddress#}{#syntax#}@frameAddress() usize{#endsyntax#}
@@ -6870,14 +7063,14 @@ export fn @"A function name that is a complete sentence."() void {}
{#header_close#} - {#header_open|@handle#} -{#syntax#}@handle(){#endsyntax#}
+ {#header_open|@frameSize#}
+ {#syntax#}@frameSize() usize{#endsyntax#}
- This function returns a {#syntax#}promise->T{#endsyntax#} type, where {#syntax#}T{#endsyntax#} - is the return type of the async function in scope. + This is the same as {#syntax#}@sizeOf(@Frame(func)){#endsyntax#}, where {#syntax#}func{#endsyntax#} + may be runtime-known.
- This function is only valid within an async function scope. + This function is typically used in conjunction with {#link|@asyncCall#}.
{#header_close#}