Files
llgo/x/async
Li Jie 3f9e86c37a x
2024-09-10 11:49:42 +08:00
..
2024-09-08 20:27:05 +08:00
2024-09-08 20:27:05 +08:00
2024-09-08 20:27:05 +08:00
x
2024-09-10 11:49:42 +08:00

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 Promise and a Future.

  • 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 a Promise or Task. It allows the function to pause its execution and resume later.

  • await: A keyword used to pause the execution of an async function until a Promise or Task is resolved. It unwraps the value of the Promise or Task and allows the function to continue.

  • co_return: A keyword used in C++ coroutines to return a value from a coroutine. It is similar to return but is used in coroutines to indicate that the coroutine has completed. It's similar to return in async functions in other languages that boxes the value into a Promise or Task.

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

  • async keyword hides the types of Promise/Future/Task in the function signature in Python and Rust, but not in JavaScript, C#, and C++.
  • await keyword unwraps the value of a Promise/Future/Task.
  • return keyword boxes the value into a Promise/Future/Task if it's not already.

Design considerations in LLGo

  • Don't introduce async/await keywords 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 chan that 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]), 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))
  })
}