12 KiB
Async I/O Design
Async functions in different languages
JavaScript
Prototype:
async function name(param0) {
statements;
}
async function name(param0, param1) {
statements;
}
async function name(param0, param1, /* …, */ paramN) {
statements;
}
Example:
async function resolveAfter1Second(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Resolved after 1 second");
}, 1000);
});
}
async function asyncCall(): Promise<string> {
const result = await resolveAfter1Second();
return `AsyncCall: ${result}`;
}
function asyncCall2(): Promise<string> {
return resolveAfter1Second();
}
function asyncCall3(): void {
resolveAfter1Second().then((result) => {
console.log(`AsyncCall3: ${result}`);
});
}
async function main() {
console.log("Starting AsyncCall");
const result1 = await asyncCall();
console.log(result1);
console.log("Starting AsyncCall2");
const result2 = await asyncCall2();
console.log(result2);
console.log("Starting AsyncCall3");
asyncCall3();
// Wait for AsyncCall3 to complete
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Main function completed");
}
main().catch(console.error);
Python
Prototype:
async def name(param0):
statements
Example:
import asyncio
async def resolve_after_1_second() -> str:
await asyncio.sleep(1)
return "Resolved after 1 second"
async def async_call() -> str:
result = await resolve_after_1_second()
return f"AsyncCall: {result}"
def async_call2() -> asyncio.Task:
return resolve_after_1_second()
def async_call3() -> None:
asyncio.create_task(print_after_1_second())
async def print_after_1_second() -> None:
result = await resolve_after_1_second()
print(f"AsyncCall3: {result}")
async def main():
print("Starting AsyncCall")
result1 = await async_call()
print(result1)
print("Starting AsyncCall2")
result2 = await async_call2()
print(result2)
print("Starting AsyncCall3")
async_call3()
# Wait for AsyncCall3 to complete
await asyncio.sleep(1)
print("Main function completed")
# Run the main coroutine
asyncio.run(main())
Rust
Prototype:
async fn name(param0: Type) -> ReturnType {
statements
}
Example:
use std::time::Duration;
use tokio::time::sleep;
use std::future::Future;
async fn resolve_after_1_second() -> String {
sleep(Duration::from_secs(1)).await;
"Resolved after 1 second".to_string()
}
async fn async_call() -> String {
let result = resolve_after_1_second().await;
format!("AsyncCall: {}", result)
}
fn async_call2() -> impl Future<Output = String> {
resolve_after_1_second()
}
fn async_call3() {
tokio::spawn(async {
let result = resolve_after_1_second().await;
println!("AsyncCall3: {}", result);
});
}
#[tokio::main]
async fn main() {
println!("Starting AsyncCall");
let result1 = async_call().await;
println!("{}", result1);
println!("Starting AsyncCall2");
let result2 = async_call2().await;
println!("{}", result2);
println!("Starting AsyncCall3");
async_call3();
// Wait for AsyncCall3 to complete
sleep(Duration::from_secs(2)).await;
println!("Main function completed");
}
C#
Prototype:
async Task<ReturnType> NameAsync(Type param0)
{
statements;
}
Example:
using System;
using System.Threading.Tasks;
class Program
{
static async Task<string> ResolveAfter1Second()
{
await Task.Delay(1000);
return "Resolved after 1 second";
}
static async Task<string> AsyncCall()
{
string result = await ResolveAfter1Second();
return $"AsyncCall: {result}";
}
static Task<string> AsyncCall2()
{
return ResolveAfter1Second();
}
static void AsyncCall3()
{
_ = Task.Run(async () =>
{
string result = await ResolveAfter1Second();
Console.WriteLine($"AsyncCall3: {result}");
});
}
static async Task Main()
{
Console.WriteLine("Starting AsyncCall");
string result1 = await AsyncCall();
Console.WriteLine(result1);
Console.WriteLine("Starting AsyncCall2");
string result2 = await AsyncCall2();
Console.WriteLine(result2);
Console.WriteLine("Starting AsyncCall3");
AsyncCall3();
// Wait for AsyncCall3 to complete
await Task.Delay(1000);
Console.WriteLine("Main method completed");
}
}
C++ 20 Coroutines
Prototype:
TaskReturnType NameAsync(Type param0)
{
co_return co_await expression;
}
Example:
#include <cppcoro/task.hpp>
#include <cppcoro/sync_wait.hpp>
#include <cppcoro/when_all.hpp>
#include <chrono>
#include <iostream>
#include <thread>
cppcoro::task<std::string> resolveAfter1Second() {
co_await std::chrono::seconds(1);
co_return "Resolved after 1 second";
}
cppcoro::task<std::string> asyncCall() {
auto result = co_await resolveAfter1Second();
co_return "AsyncCall: " + result;
}
cppcoro::task<std::string> asyncCall2() {
return resolveAfter1Second();
}
cppcoro::task<void> asyncCall3() {
auto result = co_await resolveAfter1Second();
std::cout << "AsyncCall3: " << result << std::endl;
}
cppcoro::task<void> main() {
std::cout << "Starting AsyncCall" << std::endl;
auto result1 = co_await asyncCall();
std::cout << result1 << std::endl;
std::cout << "Starting AsyncCall2" << std::endl;
auto result2 = co_await asyncCall2();
std::cout << result2 << std::endl;
std::cout << "Starting AsyncCall3" << std::endl;
auto asyncCall3Task = asyncCall3();
// Wait for AsyncCall3 to complete
co_await asyncCall3Task;
std::cout << "Main function completed" << std::endl;
}
int main() {
try {
cppcoro::sync_wait(::main());
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
Common concepts
Promise, Future, Task, and Coroutine
-
Promise: An object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It is used to produce a value that will be consumed by a
Future. -
Future: An object that represents the result of an asynchronous operation. It is used to obtain the value produced by a
Promise. -
Task: A unit of work that can be scheduled and executed asynchronously. It is a higher-level abstraction that combines a
Promiseand aFuture. -
Coroutine: A special type of function that can suspend its execution and return control to the caller without losing its state. It can be resumed later, allowing for asynchronous programming.
async, await and similar keywords
-
async: A keyword used to define a function that returns aPromiseorTask. It allows the function to pause its execution and resume later. -
await: A keyword used to pause the execution of anasyncfunction until aPromiseorTaskis resolved. It unwraps the value of thePromiseorTaskand allows the function to continue. -
co_return: A keyword used in C++ coroutines to return a value from a coroutine. It is similar toreturnbut is used in coroutines to indicate that the coroutine has completed. It's similar toreturninasyncfunctions in other languages that boxes the value into aPromiseorTask.
async/await and similar constructs provide a more readable and synchronous-like way of writing asynchronous code, it hides the type of Promise/Future/Task from the user and allows them to focus on the logic of the code.
Executing Multiple Async Operations Concurrently
To run multiple promises concurrently, JavaScript provides Promise.all, Promise.allSettled and Promise.any, Python provides asyncio.gather, Rust provides tokio::try_join, C# provides Task.WhenAll, and C++ provides cppcoro::when_all.
In some situations, you may want to get the first result of multiple async operations. JavaScript provides Promise.race to get the first result of multiple promises. Python provides asyncio.wait to get the first result of multiple coroutines. Rust provides tokio::select! to get the first result of multiple futures. C# provides Task.WhenAny to get the first result of multiple tasks. C++ provides cppcoro::when_any to get the first result of multiple tasks. Those functions are very simular to select in Go.
Error Handling
await commonly unwraps the value of a Promise or Task, but it also propagates errors. If the Promise or Task is rejected or throws an error, the error will be thrown in the async function by the await keyword. You can use try/catch blocks to handle errors in async functions.
Common patterns
asynckeyword hides the types ofPromise/Future/Taskin the function signature in Python and Rust, but not in JavaScript, C#, and C++.awaitkeyword unwraps the value of aPromise/Future/Task.returnkeyword boxes the value into aPromise/Future/Taskif it's not already.
Design considerations in LLGo
- Don't introduce
async/awaitkeywords to compatible with Go - For performance and memory reasons don't implement async functions with goroutines, coroutines, or other mechanisms that require per-task stack allocation
- Avoid implementing async task by using
chanthat blocking the thread
Design
async.Future[T] type
Introduce async.Future[T] type to represent an eventual completion (or failure) of an asynchronous operation and its resulting value, similar to Promise/Future in other languages. Functions that return async.Future[T] are considered asynchronous functions.
Future creation
async.Future[T] can be created by async.Async[T] function that takes a function that accepts a resolve function to produce a value of type T.
Future chaining (asynchronous callbacks style)
async.Future[T] can be chained with Then method to add multiple callbacks to be executed when the operation is completed, it just runs once and calls every callbacks. Currently Then method can't be chained multiple times because Go doesn't support generics method (Need support func (f Future[T]) Then[U any](f func(T) Future[U]) Future[U]) and function overload currently, maybe implements in Go+.
Future waiting (synchronous style)
async.Await[T] function can be used to wait for the completion of a Future[T] and return the value produced by the operation. In LLGo, async.Await[T] is a blocking function that waits for the completion of the Future[T] and returns the value synchronously, it would be transformed to Future.Then callback in the frontend.
async.Run[T] function
async.Run[T] function can be used to create an global asynchronous context and run async functions, and it would be hidden by the compiler in the future.
Currently it will switch the callbacks to the goroutine that calls async.Run[T] function, this maybe changed in the future to reduce the overhead of switching goroutines and make it more parallel.
Prototype
package async
type Future[T any] interface {
Then(f func(T))
}
func Async[T any](f func(resolve func(T))) Future[T]
func Await[T any](future Future[T]) T
Some async functions
package async
func Race[T1 any](futures ...Future[T1]) Future[T1]
func All[T1 any](futures ...Future[T1]) Future[[]T1]
Example
package main
func main() {
async.Run(func() {
hello := func() async.Future[string] {
return async.Async(func(resolve func(string)) {
resolve("Hello, World!")
})
}
future := hello()
future.Then(func(value string) {
println("first callback:", value)
})
future.Then(func(value string) {
println("second callback:", value)
})
println("first await:", async.Await(future))
println("second await:", async.Await(future))
})
}