async_simple

repository·main·Indexed 24 days ago

https://github.com/alibaba/async_simple

A lightweight C++ asynchronous framework providing components such as Lazy (C++20 stackless coroutines), Uthread (stackful coroutines), and traditional Future/Promise patterns. It includes an Executor interface for custom scheduling, synchronization primitives like ConditionVariable and Notifier, and support for Bazel and CMake build systems. Requires C++20.

Tokens
41K
Snippets
93
Records
155
Agent score
79%

What's inside async_simple

  1. What is RescheduleLazy and how to create it

    main

    A RescheduleLazy is a Lazy that is bound to an Executor. When a RescheduleLazy is started (via .start or syncAwait), the task that wakes it up is submitted to the bound Executor.

    You cannot create a RescheduleLazy directly. You must create it using the .via(executor) interface on an existing Lazy object.

    void foo() {
        executors::SimpleExecutor e1(1);
        auto addOne = [&](int x) -> Lazy<int> {
            auto tmp = co_await getValue(x);
            co_return tmp + 2;
        };
        // Create RescheduleLazy using .via()
        RescheduleLazy Scheduled = addOne().via(&e1);
        syncAwait(Scheduled); 
    }
  2. What is uthread and how does it work?

    main
    In async_simple, uthread provides stackful coroutines (as opposed to the standard C++20 stackless coroutines). It works by saving and restoring the register context and stack of a coroutine, allowing for cooperative multitasking similar to other industry-standard stackful coroutine libraries. It is based on the boost library.
  3. What is Try and how to use it

    main

    A Try<T> object represents a value that might contain an exception. It is used in async_simple to represent results that could fail, such as the arguments in a Lazy::start callback or a Future::thenTry callback. The concept is inspired by Facebook's Folly library.

    A Try<T> can be in one of three states:

    1. No state (uninitialized/empty)
    2. Has error (contains an exception)
    3. Has value (contains a value of type T)
  4. What is HybridCoro and when to use it

    main

    HybridCoro is a design pattern used in async_simple to combine the advantages of both stackless and stackful coroutines. It is intended for scenarios where a request travels through a deep call chain before reaching modules that perform high-concurrency asynchronous I/O.

    When to use which coroutine type:

    • Stackless Coroutine (Lazy): Use when you need to handle extremely high concurrency and require minimal switching overhead. Note that these are intrusive (the caller must also be a stackless coroutine) and performance can degrade as the call chain depth increases due to memory allocation and parameter copying.
    • Stackful Coroutine (Uthread): Use for deep call chains where you want to avoid the overhead of stackless coroutines. These are non-intrusive (except for asynchronous I/O) and their performance is not dependent on the depth of the call chain, though they may have lower performance than stackless coroutines under very high concurrency.

    The Hybrid Approach

    In production, a common pattern is to wrap a user's request in a stackful coroutine to handle the deep business logic/call chains, and then use stackless coroutines within the specific modules that need to execute a large number of concurrent queries. This hybrid model mitigates the intrusiveness of stackless coroutines while maintaining high performance for high-concurrency tasks.

  5. What is Lazy and how to use it

    main

    A Lazy<T> represents a lazy evaluation of a computation task implemented using C++20 stackless coroutines. To use it, include <async_simple/coro/Lazy.h> and define a coroutine function that returns Lazy<T>.

    Alignment Restriction: Due to ABI and implementation constraints, the alignment requirement of T in Lazy<T> must not exceed alignof(std::max_align_t) (typically 16).

    #include <async_simple/coro/Lazy.h>
    
    // A coroutine function returning Lazy<int>
    Lazy<int> task1(int x) {
        co_return x;
    }
    
    // A Lazy that co_awaits another awaitable
    Lazy<int> task2(int x) {
        co_await std::suspend_always{};
        co_return x;
    }
  6. How signals and slots work for task cancellation

    main

    async-simple uses a collaborative signal-slot model to provide a thread-safe and efficient asynchronous task cancellation mechanism.

    • Signal: Used to initiate/emit signals. A single Signal can have multiple Slots bound to it. When a Signal is emitted, all bound Slots receive the signal.
    • Slot: Used to receive signals. Each asynchronous task should hold its own Slot.

    Key Lifecycle Rule: The lifetime of a Signal is automatically extended until the last bound Slot is destroyed. You can safely access the signal from a slot using slot->signal().

  7. Chain multiple Signals together

    main

    You can create a hierarchy of signals using addChainedSignal. When a parent Signal is triggered, the signal is automatically forwarded to all its chained child Signals.

    Note on directionality: Signal forwarding is one-way. A signal triggered in a child Signal will not be forwarded back to the parent Signal.

    std::shared_ptr<Signal> signal = Signal::create();
    auto slot = std::make_unique<Slot>(signal.get());
    std::shared_ptr<Signal> chainedSignal = Signal::create();
    
    // Forward signals from 'signal' to 'chainedSignal'
    slot->addChainedSignal(chainedSignal);
    
    signal->emits(SignalType::terminate);
    assert(chainedSignal->state() == SignalType::terminate);
    
    // This will NOT affect the parent 'signal'
    chainedSignal->emits(static_cast<SignalType>(0b10));
    assert(signal->state() != static_cast<SignalType>(0b10));
  8. Use the Try abstraction to handle potential exceptions

    main

    The Try<T> type represents a result that can be in one of three states: Nothing, Exception, or Value (of type T). It is used throughout async_simple (e.g., in Lazy::start or Future::thenTry callbacks) to encapsulate a value or an error.

    State Management

    • Nothing: The state of a Try object created with the default constructor. Use .available() to check if the object is in the Nothing state.
    • Exception: The state when the Try contains an error. Use .hasError() to check for this state.
    • Value: The state when the Try contains a successful result of type T.

    Accessing Data

    • Use .value() to retrieve the contained value. Warning: If the Try state is Exception, calling .value() will throw the contained exception.
    • Use .getException() to retrieve the std::exception_ptr if the state is Exception.
  9. Use Promise and Future for asynchronous communication

    main

    In async_simple, Promise<T> and Future<T> act as a communication channel between different execution contexts (stackful coroutines, stackless coroutines, or normal functions). A Promise is used to set a value or an exception, and a Future is used to retrieve that result.

    Key behaviors:

    • A Promise and Future pair is one-to-one.
    • A value can be set at most once.
    • Use async_simple::Unit as the template argument if you do not need to return a specific value type.

    To retrieve the value:

    • Future::wait(): Blocks until the value is ready.
    • Future::value(): Returns the value (call after wait()).
    • Future::get(): Blocks and returns the value directly.
    • Future::result(): Returns a Try<T> containing either the value or an exception. This is the recommended way to handle potential errors using Try::hasError().
  10. Understand Hybrid Coroutines in async_simple

    main

    Hybrid Coroutines combine the advantages of Stackful (Uthread) and Stackless (Lazy) coroutines to optimize performance in complex business logic.

    In a typical high-performance scenario, a user request travels through deep function call stacks before reaching low-level modules that perform heavy asynchronous I/O.

    The Hybrid Model Strategy:

    1. Use Stackful Coroutines (Uthread) for the high-level request handling: This allows the business logic to remain non-intrusive (no need for co_await or C++20 coroutine keywords) and handles deep call stacks efficiently without performance degradation.
    2. Use Stackless Coroutines (Lazy) for low-level I/O modules: When performing massive parallel I/O operations, stackless coroutines provide extremely high switching performance with minimal overhead, making them ideal for high-concurrency asynchronous tasks.

    By combining them, you achieve a system where the upstream business code requires no modification to become asynchronous, while the downstream I/O layer maintains maximum concurrency efficiency.

  11. Use Semaphore for coroutine synchronization

    main

    The async_simple::coro::Semaphore is similar to std::counting_semaphore but is specifically designed for use with Lazy coroutines. It provides mechanisms to control access to resources or signal between coroutines using acquire() and release() operations.

    Common use cases include:

    • Notifier: Using a BinarySemaphore to signal when a task is ready.
    • Mutex: Using a BinarySemaphore initialized with a count of 1 to ensure mutual exclusion for shared data.
    #include <async_simple/coro/Semaphore>
    
    using namespace async_simple::coro;
    
    // Example: Using Semaphore as a Mutex
    BinarySemaphore sem(1);
    int count = 0;
    
    Lazy<> producer() {
      co_await sem.acquire();
      ++count;
      co_await sem.release();
      co_return;
    }
    
    Lazy<> consumer() {
      co_await sem.acquire();
      --count;
      co_await sem.release();
      co_return;
    }