libunifex Documentation

repository·main·Indexed 23 days ago

https://github.com/facebookexperimental/libunifex

A prototype implementation of the C++ sender/receiver asynchronous programming model. libunifex provides primitives for schedulers, timers, async I/O (via Linux io_uring), concurrency pattern algorithms, async streams, cancellation mechanisms, and coroutine integration. It includes utilities for creating custom senders via create_raw_sender and create_basic_sender, as well as algorithms like then, let_value, and defer for managing asynchronous workflows.

Tokens
28K
Snippets
43
Records
96
Agent score
82%

What's inside libunifex

  1. Overview of libunifex

    main

    libunifex is a prototype implementation of the C++ sender/receiver asynchronous programming model (currently under consideration for standardization). It provides implementations for:

    • Schedulers and Timers
    • Asynchronous I/O (via Linux io_uring)
    • Algorithms for concurrency patterns
    • Async streams
    • Cancellation mechanisms
    • Coroutine integration

    Note: This project is experimental. API and ABI stability are not guaranteed.

  2. What is a TimeScheduler and its capabilities

    main

    A TimeScheduler is a specialized Scheduler that can schedule work to occur at or after a specific point in time. It integrates time management directly with the scheduler to allow for features like virtual time in unit tests.

    Capabilities

    • now(ts): Returns the current time_point from the scheduler.
    • schedule_at(ts, time_point): Returns a sender_of<void> that completes at the specified time_point.
    • schedule_after(ts, duration): Returns a sender_of<void> that completes after the specified duration has elapsed.
  3. What is a Scheduler and how to use it

    main

    A scheduler is a lightweight handle representing an execution context. It provides a schedule() operation that returns a Sender (a "sender of void").

    Using schedule() to execute work

    When the schedule() operation completes successfully (set_value()), the work is guaranteed to be performed on the scheduler's associated execution context. To execute specific work on that context, perform the work inside the set_value() call of the receiver.

    Sub-schedulers

    To schedule work back onto the same execution context (for example, a specific thread within a thread pool), use schedule_with_subscheduler(). This returns a Sender that, upon completion, delivers a Scheduler representing the current execution context as its value.

  4. What is the sender/receiver model in libunifex?

    main

    libunifex is an implementation of the C++ sender/receiver asynchronous programming model.

    In this model, a sender is a reification of an operation that delivers its result by calling a function (a receiver) rather than returning a value. This allows asynchronous operations to be composed using higher-order algorithms and executed lazily.

    Key components provided by the library include:

    • Schedulers
    • Timers
    • Asynchronous I/O (Linux with io_uring)
    • Algorithms for concurrency patterns
    • Async streams
    • Cancellation support
    • Coroutine integration
  5. Use `async_pass` for synchronized cross-scheduler rendezvous

    main

    async_pass (and nothrow_async_pass) provides a rendezvous point for synchronized cross-scheduler calls between a caller (producer) and an acceptor (consumer). Unlike a 1-element queue, the caller is guaranteed that the acceptor was ready and has accepted the call.

    Key Concepts

    • Arguments: Args... defines the value types of the sender returned by async_accept().
    • nothrow_async_pass: Does not provide try_throw() or async_throw() and requires argument types to be nothrow (copy/move) constructible.
    • Lifetime Requirement: Arguments passed to async_call() MUST NOT go out of scope until the resulting sender has started. In coroutines, this is handled by co_awaiting the sender immediately.

    API Reference

    • async_call(auto&&... args): Passes data to the acceptor. Completes when the acceptor accepts or is cancelled.
    • async_call(auto&& fn): Deferred version. fn is a callback activated at rendezvous.
    • async_throw(ex): Passes an exception to the acceptor.
    • async_accept(): Completes when a caller calls async_call() or try_call().
    • try_call(args...): Synchronously completes the rendezvous if an acceptor is waiting.
    • try_call(fn): Deferred synchronous version.
    • try_throw(ex): Synchronously passes an exception to an awaiting acceptor.
    • try_accept(): Synchronously completes the rendezvous by returning data or throwing if a caller is waiting.
    • try_accept(fn): Deferred synchronous version.
    • is_idle(): Returns true if no one is awaiting.
    • is_expecting_call(): Returns true if an acceptor is awaiting.
    • is_expecting_accept(): Returns true if a caller is awaiting.
    template <typename... Args> class async_pass;
    template <typename... Args> class nothrow_async_pass;
    
    // Example of a lifetime error to avoid:
    auto sender = pass.async_call(Request{});
    // Temporary Request{} is destroyed, but sender kept a reference to it
    co_await std::move(sender);
  6. How V-Tables work in libunifex type erasure

    main

    In libunifex, type-erased wrappers use V-Tables to manage polymorphic behavior without standard C++ inheritance. A V-Table is constructed from a set of CPOs (Customization Point Objects) and consists of function pointers for each CPO.

    Core Components

    • unifex::detail::vtable_entry<CPO>: An individual entry in a V-Table holding a function pointer that type-erases a specific CPO implementation.
    • unifex::detail::vtable<CPOs...>: An ordered collection of vtable_entry objects.

    CPO Signature Requirements

    To use a CPO in a V-Table, the CPO must define a type_erased_signature_t. This signature must be a function type containing exactly one argument that is either a unifex::this_ type (or a cv-qualified version) or an lvalue/xvalue reference to such a type.

    Example CPO definition:

    struct my_cpo {
      using type_erased_signature_t = void(const unifex::this_&, float, bool);
    };

    When a vtable_entry is invoked, it internally casts a void* (the managed object) back to the concrete type and replaces the unifex::this_ parameter with a reference to that concrete object.

    struct my_cpo {
      using type_erased_signature_t = void(const unifex::this_&, float, bool);
    };
  7. Implement TypedManySender

    main

    A TypedManySender is an extension of both the ManySender and TypedSender concepts.

    To implement a TypedManySender, you must provide:

    1. next_types: A type-alias describing the types passed to set_next().
    2. value_types: A type-alias describing the types passed to set_value().
    3. error_types: A type-alias describing the types passed to set_error().
    4. sends_done: A static constexpr bool indicating if set_done() is used.

    Note: Because TypedManySender requires next_types, the TypedSender concept does not automatically subsume the TypedManySender concept.

  8. How cancellation works in libunifex

    main

    Cancellation is an intrinsic part of the libunifex asynchronous model. It is designed to allow high-level operations (e.g., downloading a file) to propagate cancellation requests down to low-level operations (e.g., reading from a socket or waiting for a timer).

    This ensures that when a higher-level goal is satisfied or no longer needed, resources are not wasted on concurrent operations that are no longer relevant. The model is designed so that if cancellation is not required, there is no runtime overhead compared to code without cancellation support.

  9. Understand the StopToken concept for cancellation

    main

    Unifex uses a generic stop_token_concept to support the cancellation of asynchronous operations. A stop-token is passed to an operation to allow a request for that operation to stop executing (e.g., when its result is no longer needed).

    Unlike standard C++20 std::stop_token, Unifex uses a concept-based approach to allow for more efficient implementations, such as avoiding heap allocation or reference counting in structured concurrency scenarios.

    Key requirements for a type to satisfy stop_token_concept:

    • Must be std::copyable and support no-throw copy/move construction.
    • Must provide a nested template type alias: typename T::template callback_type<CallbackArchetype>.
    • Must provide stop_requested() and stop_possible() methods that are noexcept.
    • The callback_type must be destructible and support no-throw construction from the token and a callback archetype.

    Note on std::stop_token compatibility: Currently, std::stop_token does not satisfy the Unifex stop_token_concept because it lacks the required nested callback_type template type alias.

    namespace unifex
    {
      struct __stop_token_callback_archetype {
        // These have no definitions.
        __stop_token_callback_archetype() noexcept;
        __stop_token_callback_archetype(__stop_token_callback_archetype&&) noexcept;
        __stop_token_callback_archetype(const __stop_token_callback_archetype&) noexcept;
        ~__stop_token_callback_archetype();
        void operator()() noexcept;
      };
    
      template<typename T>
      concept stop_token_concept = 
        std::copyable<T> &&
        std::is_nothrow_copy_constructible_v<T> &&
        std::is_nothrow_move_constructible_v<T> &&
        requires(const T token) {
          typename T::template callback_type<__stop_token_callback_archetype>;
          { token.stop_requested() ? (void)0 : (void)0 } noexcept;
          { token.stop_possible() ? (void)0 : (void)0 } noexcept;
        } &&
        std::destructible<
          typename T::template callback_type<__stop_token_callback_archetype>> &&
        std::is_nothrow_constructible_v(
          typename T::template callback_type<__stop_token_callback_archetype>,
          T, __stop_token_callback_archetype) &&
        std::is_nothrow_constructible_v(
          typename T::template callback_type<__stop_token_callback_archetype>,
          const T&, __stop_token_callback_archetype);
    }
  10. How type erasure works in libunifex

    main

    libunifex uses type erasure to manage compile times and implement polymorphism by insulating different code components from each other.

    The library provides generic type-erasing wrappers built on top of tag_invoke-based Customisation Point Objects (CPOs). These wrappers are parameterized by a variadic list of CPOs. These CPOs define the specific operations that a concrete type must support and which the type-erased wrapper will expose to the user.

  11. Compare Scheduler Types

    main

    Libunifex provides several scheduler implementations for different concurrency needs:

    SchedulerBehavior
    inline_schedulerImmediately invokes the receiver inline upon calling start().
    single_thread_contextSpawns a single background thread for scheduled tasks. Use .get_scheduler() to get its scheduler.
    trampoline_schedulerAn inline scheduler that limits recursion depth, scheduling subsequent work once the stack unwinds.
    timed_single_thread_contextA single-threaded context supporting schedule_at(time_point) and schedule_after(duration).
    thread_unsafe_event_loopAssumes all access is from the same thread (no internal synchronization). Supports timed scheduling.
    new_thread_contextSpawns a new thread for every schedule() call. Joins all threads on destruction.
    linux::io_uring_contextLinux-specific I/O event loop using io_uring. Requires calling .run().
  12. Implement the TypedSender concept

    main

    A TypedSender is a Sender that provides nested template type-aliases to allow consumers to query the specific overloads of set_value() and set_error() it may call on a Receiver.

    To implement a TypedSender, you must define:

    • value_types: A template alias taking a Variant and a Tuple template. It produces a type representing all possible set_value call signatures.
    • error_types: A template alias taking a Variant template. It produces a type representing all possible set_error call signatures.
    • sends_done: A static constexpr bool indicating if the sender might call set_done().

    When querying these properties, use unifex::sender_traits<Sender> rather than accessing the sender type directly.

    struct some_typed_sender {
     template<template<typename...> class Variant,
              template<typename...> class Tuple>
     using value_types = Variant<Tuple<int>,
                                 Tuple<std::string, int>,
                                 Tuple<>>;
    
     template<template<typename...> class Variant>
     using error_types = Variant<std::exception_ptr>;
    
     static constexpr bool sends_done = true;
     ...
    };
    
    // To query:
    // typename unifex::sender_traits<Sender>::template value_types<std::variant, std::tuple>