zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit d3672493cc6ad5085f202df1859b13b4ae4dec96 (tree)
parent 55f5cee86b39bb2127a316f9b5d0abf532580cac
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Thu, 15 Aug 2019 16:46:43 -0400

basic docs for new async/await semantics

Diffstat:
Mdoc/langref.html.in | 423+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
1 file changed, 308 insertions(+), 115 deletions(-)

diff --git a/doc/langref.html.in b/doc/langref.html.in @@ -5970,54 +5970,25 @@ test "global assembly" { {#header_close#} {#header_open|Async Functions#} <p> - An async function is a function whose callsite is split into an {#syntax#}async{#endsyntax#} initiation, - followed by an {#syntax#}await{#endsyntax#} completion. - </p> - <p> - 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. - </p> - <p> - 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. + 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. </p> - {#header_open|Minimal Async Function Example#} <p> - Declare an async function with the {#syntax#}async{#endsyntax#} keyword. - The expression in angle brackets must evaluate to a struct - which has these fields: - </p> - <ul> - <li>{#syntax#}allocFn: fn (self: *Allocator, byte_count: usize, alignment: u29) Error![]u8{#endsyntax#} - where {#syntax#}Error{#endsyntax#} can be any error set.</li> - <li>{#syntax#}freeFn: fn (self: *Allocator, old_mem: []u8) void{#endsyntax#}</li> - </ul> - <p> - 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. - </p> - <p> - 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. - </p> - <p> - 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. + An async function is a function whose callsite is split into an {#syntax#}async{#endsyntax#} initiation, + 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. </p> <p> - 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#}: + Zig infers that a function is {#syntax#}async{#endsyntax#} when it observes that the function contains + a <strong>suspension point</strong>. Async functions can be called the same as normal functions. A + function call of an async function is a suspend point. </p> + {#header_open|Suspend and Resume#} <p> - 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). </p> {#code_begin|test#} const std = @import("std"); @@ -6025,32 +5996,25 @@ const assert = std.debug.assert; var x: i32 = 1; -test "call an async function" { - var frame = async simpleAsyncFn(); - comptime assert(@typeOf(frame) == @Frame(simpleAsyncFn)); +test "suspend with no resume" { + var frame = async func(); assert(x == 2); } -fn simpleAsyncFn() void { + +fn func() void { x += 1; suspend; + // This line is never reached because the suspend has no matching resume. x += 1; } {#code_end#} - {#header_close#} - {#header_open|Suspend and Resume#} <p> - 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: - </p> - <p> - TODO another test example here - </p> - <p> - 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 <strong>suspend block</strong> 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. </p> {#code_begin|test#} const std = @import("std"); @@ -6061,9 +6025,9 @@ var result = false; test "async function suspend with block" { _ = async testSuspendBlock(); - std.debug.assert(!result); + assert(!result); resume the_frame; - std.debug.assert(result); + assert(result); } fn testSuspendBlock() void { @@ -6075,19 +6039,15 @@ fn testSuspendBlock() void { } {#code_end#} <p> - 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. - </p> - <p> - {#link|Await#} counts as a suspend point. + {#syntax#}suspend{#endsyntax#} causes a function to be {#syntax#}async{#endsyntax#}. </p> + {#header_open|Resuming from Suspend Blocks#} <p> 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. </p> <p> @@ -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#} <p> - 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. </p> {#header_close#} {#header_close#} - {#header_open|Await#} + + {#header_open|Async and Await#} <p> - The {#syntax#}await{#endsyntax#} keyword is used to coordinate with an async function's - {#syntax#}return{#endsyntax#} statement. + 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#}. </p> + {#code_begin|test#} +const std = @import("std"); +const assert = std.debug.assert; + +test "async and await" { + // Here we have an exception where we do not match an async + // with an await. The test block is not async and so cannot + // have a suspend point in it. + // This is well-defined behavior, and everything is OK here. + // Note however that there would be no way to collect the + // return value of amain, if it were something other than void. + _ = async amain(); +} + +fn amain() void { + var frame = async func(); + comptime assert(@typeOf(frame) == @Frame(func)); + + const ptr: anyframe->void = &frame; + const any_ptr: anyframe = ptr; + + resume any_ptr; + await ptr; +} + +fn func() void { + suspend; +} + {#code_end#} <p> - {#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. + The {#syntax#}await{#endsyntax#} keyword is used to coordinate with an async function's + {#syntax#}return{#endsyntax#} statement. </p> <p> - A frame handle must be consumed exactly once after it is created with {#syntax#}await{#endsyntax#}. + {#syntax#}await{#endsyntax#} is a suspend point, and takes as an operand anything that + implicitly casts to {#syntax#}anyframe->T{#endsyntax#}. </p> <p> - {#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. </p> {#code_begin|test#} const std = @import("std"); @@ -6156,14 +6143,14 @@ test "async function await" { assert(final_result == 1234); assert(std.mem.eql(u8, seq_points, "abcdefghi")); } -async fn amain() void { +fn amain() void { seq('b'); var f = async another(); seq('e'); final_result = await f; seq('h'); } -async fn another() i32 { +fn another() i32 { seq('c'); suspend { seq('d'); @@ -6183,31 +6170,156 @@ fn seq(c: u8) void { {#code_end#} <p> 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. </p> {#header_close#} - {#header_open|Open Issues#} + + {#header_open|Async Function Example#} <p> - 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: + </p> + {#code_begin|exe|async#} +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub fn main() void { + _ = async amainWrap(); + + // Typically we would use an event loop to manage resuming async functions, + // but in this example we hard code what the event loop would do, + // to make things deterministic. + resume global_file_frame; + resume global_download_frame; +} + +fn amainWrap() void { + amain() catch |e| { + std.debug.warn("{}\n", e); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + std.process.exit(1); + }; +} + +fn amain() !void { + const allocator = std.heap.direct_allocator; + var download_frame = async fetchUrl(allocator, "https://example.com/"); + var awaited_download_frame = false; + errdefer if (!awaited_download_frame) { + if (await download_frame) |r| allocator.free(r) else |_| {} + }; + + var file_frame = async readFile(allocator, "something.txt"); + var awaited_file_frame = false; + errdefer if (!awaited_file_frame) { + if (await file_frame) |r| allocator.free(r) else |_| {} + }; + + awaited_file_frame = true; + const file_text = try await file_frame; + defer allocator.free(file_text); + + awaited_download_frame = true; + const download_text = try await download_frame; + defer allocator.free(download_text); + + std.debug.warn("download_text: {}\n", download_text); + std.debug.warn("file_text: {}\n", file_text); +} + +var global_download_frame: anyframe = undefined; +fn fetchUrl(allocator: *Allocator, url: []const u8) ![]u8 { + const result = try std.mem.dupe(allocator, u8, "this is the downloaded url contents"); + errdefer allocator.free(result); + suspend { + global_download_frame = @frame(); + } + std.debug.warn("fetchUrl returning\n"); + return result; +} + +var global_file_frame: anyframe = undefined; +fn readFile(allocator: *Allocator, filename: []const u8) ![]u8 { + const result = try std.mem.dupe(allocator, u8, "this is the file contents"); + errdefer allocator.free(result); + suspend { + global_file_frame = @frame(); + } + std.debug.warn("readFile returning\n"); + return result; +} + {#code_end#} + <p> + Now we remove the {#syntax#}suspend{#endsyntax#} and {#syntax#}resume{#endsyntax#} code, and + observe the same behavior, with one tiny difference: + </p> + {#code_begin|exe|blocking#} +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub fn main() void { + _ = async amainWrap(); +} + +fn amainWrap() void { + amain() catch |e| { + std.debug.warn("{}\n", e); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + std.process.exit(1); + }; +} + +fn amain() !void { + const allocator = std.heap.direct_allocator; + var download_frame = async fetchUrl(allocator, "https://example.com/"); + var awaited_download_frame = false; + errdefer if (!awaited_download_frame) { + if (await download_frame) |r| allocator.free(r) else |_| {} + }; + + var file_frame = async readFile(allocator, "something.txt"); + var awaited_file_frame = false; + errdefer if (!awaited_file_frame) { + if (await file_frame) |r| allocator.free(r) else |_| {} + }; + + awaited_file_frame = true; + const file_text = try await file_frame; + defer allocator.free(file_text); + + awaited_download_frame = true; + const download_text = try await download_frame; + defer allocator.free(download_text); + + std.debug.warn("download_text: {}\n", download_text); + std.debug.warn("file_text: {}\n", file_text); +} + +fn fetchUrl(allocator: *Allocator, url: []const u8) ![]u8 { + const result = try std.mem.dupe(allocator, u8, "this is the downloaded url contents"); + errdefer allocator.free(result); + std.debug.warn("fetchUrl returning\n"); + return result; +} + +fn readFile(allocator: *Allocator, filename: []const u8) ![]u8 { + const result = try std.mem.dupe(allocator, u8, "this is the file contents"); + errdefer allocator.free(result); + std.debug.warn("readFile returning\n"); + return result; +} + {#code_end#} + <p> + 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. </p> - <ul> - <li>Async functions have optimizations disabled - even in release modes - due to an - <a href="https://github.com/ziglang/zig/issues/802">LLVM bug</a>. - </li> - <li> - There are some situations where we can know statically that there will not be - memory allocation failure, but Zig still forces us to handle it. - TODO file an issue for this and link it here. - </li> - <li> - Zig does not take advantage of LLVM's allocation elision optimization for - async function. It crashed LLVM when I tried to do it the first time. This is - related to the other 2 bullet points here. See - <a href="https://github.com/ziglang/zig/issues/802">#802</a>. - </li> - </ul> {#header_close#} {#header_close#} @@ -6265,6 +6377,49 @@ comptime { Note: This function is deprecated. Use {#link|@typeInfo#} instead. </p> {#header_close#} + + {#header_open|@asyncCall#} + <pre>{#syntax#}@asyncCall(frame_buffer: []u8, result_ptr, function_ptr, args: ...) anyframe->T{#endsyntax#}</pre> + <p> + {#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#}. + </p> + <p> + 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#}. + </p> + <p> + {#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#}. + </p> + {#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#} <pre>{#syntax#}@atomicLoad(comptime T: type, ptr: *const T, comptime ordering: builtin.AtomicOrder) T{#endsyntax#}</pre> <p> @@ -6855,6 +7010,44 @@ export fn @"A function name that is a complete sentence."() void {} {#see_also|@intToFloat#} {#header_close#} + {#header_open|@frame#} + <pre>{#syntax#}@frame() *@Frame(func){#endsyntax#}</pre> + <p> + 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. + </p> + <p> + This function does not mark a suspension point, but it does cause the function in scope + to become an {#link|async function|Async Functions#}. + </p> + {#header_close#} + + {#header_open|@Frame#} + <pre>{#syntax#}@Frame(func: var) type{#endsyntax#}</pre> + <p> + 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. + </p> + <p> + 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: + </p> + {#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#} <pre>{#syntax#}@frameAddress() usize{#endsyntax#}</pre> <p> @@ -6870,14 +7063,14 @@ export fn @"A function name that is a complete sentence."() void {} </p> {#header_close#} - {#header_open|@handle#} - <pre>{#syntax#}@handle(){#endsyntax#}</pre> + {#header_open|@frameSize#} + <pre>{#syntax#}@frameSize() usize{#endsyntax#}</pre> <p> - 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. </p> <p> - This function is only valid within an async function scope. + This function is typically used in conjunction with {#link|@asyncCall#}. </p> {#header_close#}