basic docs for new async/await semantics

This commit is contained in:
Andrew Kelley 2019-08-15 16:46:43 -04:00
parent 55f5cee86b
commit d3672493cc
No known key found for this signature in database
GPG Key ID: 7C5F548F728501A9

View File

@ -5970,54 +5970,25 @@ test "global assembly" {
{#header_close#}
{#header_open|Async Functions#}
<p>
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>
<p>
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.
</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.
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>
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.
</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.
</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#}:
</p>
<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>
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>
The {#syntax#}await{#endsyntax#} keyword is used to coordinate with an async function's
{#syntax#}return{#endsyntax#} statement.
{#syntax#}return{#endsyntax#} statement.
</p>
<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.
{#syntax#}await{#endsyntax#} is a suspend point, and takes as an operand anything that
implicitly casts to {#syntax#}anyframe->T{#endsyntax#}.
</p>
<p>
A frame handle must be consumed exactly once after it is created with {#syntax#}await{#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#}