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 compiler (just compiling) - For performance reason don't implement async functions with goroutines
- Avoid implementing
Promiseby usingchanto avoid blocking the thread, but it can be wrapped as achanto make it compatibleselectstatement
Design
Introduce async.IO[T] type to represent an asynchronous operation, async.Future[T] type to represent the result of an asynchronous operation. async.IO[T] can be bind to a function that accepts T as an argument to chain multiple asynchronous operations. async.IO[T] can be await to get the value of the asynchronous operation.
package async
type Future[T any] func(func(T))
func Await[T any](future Future[T]) T
func main() {
hello := func() Future[string] {
return func(resolve func(string)) {
resolve("Hello, World!")
}
}
future := hello()
future(func(value string) {
println(value)
})
println(Await(future))
}