Core API

The core subfolder/namespace contains the basic building blocks of the library, this is the API to describe DAG’s and internal/backend machinery for memory management. To include just libfork’s core use #include <libfork/core.hpp>.

Enums and tags

Dispatch tags

enum class lf::core::tag

An enumeration that determines the behavior of a coroutine’s promise.

You can inspect the first argument of an async function to determine the tag.

Values:

enumerator root

This coroutine is a root task from an lf::sync_wait.

enumerator call

Non root task from an lf::call, completes synchronously.

enumerator fork

Non root task from an lf::fork, completes asynchronously.

Dispatch modifiers

namespace modifier

Modifier’s for the dispatch tag, these do not effect the child’s promise, only the awaitable.

See lf::core::dispatch for more information and uses, these are low-level and most users should not need to use them. We use a namespace + types rather than an enumeration to allow for type-concepts.

struct eager_throw
#include <libfork/core/tag.hpp>

The dispatch is a call, the awaitable will throw eagerly.

struct eager_throw_outside
#include <libfork/core/tag.hpp>

The dispatch is a call outside a fork-join scope, the awaitable will throw eagerly.

struct none
#include <libfork/core/tag.hpp>

No modification to the dispatch category.

struct sync
#include <libfork/core/tag.hpp>

The dispatch is fork, reports if the fork completed synchronously.

struct sync_outside
#include <libfork/core/tag.hpp>

The dispatch is a fork outside a fork-join scope, reports if the fork completed synchronously.

Concepts and traits

Libfork has a concept-driven API, built upon the following concepts and traits.

Building blocks

template<typename T>
concept storable
#include <libfork/core/first_arg.hpp>

Verify a forwarding reference is storable as a value type after removing cvref qualifiers.

template<typename T>
concept returnable
#include <libfork/core/task.hpp>

A type returnable from libfork’s async functions/coroutines.

This requires that T is void a reference or a std::movable type.

template<typename T>
concept referenceable
#include <libfork/core/first_arg.hpp>

Test if we can form a reference to an instance of type T.

template<typename I>
concept dereferenceable
#include <libfork/core/first_arg.hpp>

Test if the expression *std::declval<T&>() is valid and is lf::core::referenceable.

template<typename I>
concept quasi_pointer
#include <libfork/core/first_arg.hpp>

A quasi-pointer if a movable type that can be dereferenced to a lf::core::referenceable.

A quasi-pointer is assumed to be cheap-to-move like an iterator/legacy-pointer.

template<typename F>
concept async_function_object
#include <libfork/core/first_arg.hpp>

A concept that requires a type be a copyable function object.

An async function object is a function object that returns an lf::task when operator() is called. with appropriate arguments. The call to operator() must create a libfork coroutine. The first argument of an async function must accept a deduced templated-type that satisfies the lf::core::first_arg concept. The return type and invocability of an async function must be independent of the first argument except for its tag value.

An async function may be copied, its copies must be equivalent to the original and support concurrent invocation from multiple threads. It is assumed that an async function is cheap-to-copy like an iterator/legacy-pointer.

API

template<typename T>
concept first_arg
#include <libfork/core/first_arg.hpp>

This describes the public-API of the first argument passed to an async function.

An async functions’ invocability and return type must be independent of their first argument except for its tag value. A user may query the first argument’s static member tagged to obtain this value. Additionally, a user may query the first argument’s static member function context() to obtain a pointer to the current workers context. Finally a user may cache an exception in-flight by calling .stash_exception().

template<typename Sch>
concept scheduler
#include <libfork/core/scheduler.hpp>

A concept that schedulers must satisfy.

This requires only a single method, schedule which accepts an lf::submit_handle and promises to call lf::resume() on it. The schedule method must fulfill the strong exception guarantee.

template<typename T>
concept context_switcher
#include <libfork/core/scheduler.hpp>

Defines the interface for awaitables that may trigger a context switch.

A context_switcher can be awaited inside a libfork coroutine. If the awaitable is not ready then the coroutine will be suspended and a submit_handle will be passed to the context switcher’s await_suspend() function. This can then be resumed by any worker as normal.

template<typename T>
concept co_allocable
#include <libfork/core/co_alloc.hpp>

Check is a type is suitable for allocation on libfork’s stacks.

This requires the type to be std::default_initializable<T> and have non-new-extended alignment.

template<typename I>
concept stash_exception_in_return
#include <libfork/core/exceptions.hpp>

A concept that checks if a quasi-pointer can be used to stash an exception.

If the expression stash_exception(*ptr) is well-formed and noexcept and ptr is used as the return address of an async function, then if that function terminates with an exception, the exception will be stored in the quasi-pointer via a call to stash_exception.

template<typename T, tag Tag>
concept modifier_for
#include <libfork/core/tag.hpp>

Test if a type is a valid modifier for a tag.

Functional

template<typename I, tag Tag, typename F, typename ...Args>
concept async_tag_invocable
#include <libfork/core/invocable.hpp>

Check F is Tag-invocable with Args... and returns an lf::task who’s result is returnable via I.

In the following description “invoking” or “async invoking” means to call F with Args... via the appropriate libfork function i.e. fork corresponds to lf::fork[r, f](args...) and the library will generate the appropriate (opaque) first-argument.

This requires:

  • F is an async function object.

  • F is ‘Tag’/call invocable with Args... when writing the result to I or discarding it.

  • The result of all of these calls has the same type.

  • The result of all of these calls is an instance of type lf::task<R>.

  • I is movable and dereferenceable.

  • I is indirectly writable from R or R is void while I is discard_t.

  • If R is non-void then F is lf::core::async_tag_invocable when I is lf::eventually<R> *.

  • F is lf::core::async_tag_invocable when I is lf::try_eventually<R> *.

This concept is provided as a building block for higher-level concepts.

template<typename F, typename ...Args>
concept callable
#include <libfork/core/invocable.hpp>

Alias for lf::core::async_tag_invocable<lf::impl::discard_t, lf::core::tag::call, F, Args...>.

template<typename F, typename ...Args>
concept rootable
#include <libfork/core/invocable.hpp>

Test if an async function is root-invocable and call-invocable, subsumes lf::core::callable.

template<typename F, typename ...Args>
concept forkable
#include <libfork/core/invocable.hpp>

Test if an async function is fork-invocable and call-invocable, subsumes lf::core::callable.

template<typename F, typename ...Args>
concept async_invocable
#include <libfork/core/invocable.hpp>

Test if an async function is lf::core::forkable and lf::core::rootable, subsumes both.

template<typename F, typename ...Args>
using lf::async_result_t = typename async_result<F, Args...>::type

Fetch R when the async function F returns lf::task<R>.

Control flow

Sync to async

template<scheduler Sch, async_function_object F, class ...Args>
auto lf::core::schedule(Sch &&sch, F &&fun, Args&&... args) -> future<async_result_t<F, Args...>>

Schedule execution of fun on sch and return a lf::core::future to the result.

This will build a task from fun and dispatch it to sch via its schedule method. If schedule is called by a worker thread (which are never allowed to block) then lf::core::schedule_in_worker will be thrown.

template<scheduler Sch, async_function_object F, class ...Args>
auto lf::core::sync_wait(Sch &&sch, F &&fun, Args&&... args) -> async_result_t<F, Args...>

Schedule execution of fun on sch and wait (block) until the task is complete.

This is the primary entry point from the synchronous to the asynchronous world. A typical libfork program is expected to make a call from main into a scheduler/runtime by scheduling a single root-task with this function.

This makes the appropriate call to lf::core::schedule and calls get on the returned lf::core::future.

template<scheduler Sch, async_function_object F, class ...Args>
auto lf::core::detach(Sch &&sch, F &&fun, Args&&... args) -> void

Schedule execution of fun on sch and detach the future.

This is the secondary entry point from the synchronous to the asynchronous world. Similar to sync_wait but calls detach on the returned lf::core::future.

Note: Many schedulers (like lf::lazy_pool and lf::busy_pool) require all submitted work to (including detached work) to complete before they are destructed.

Fork-join

constexpr auto lf::core::fork = dispatch<tag::fork>

A second-order functor used to produce an awaitable (in an lf::task) that will trigger a fork.

Conceptually the forked/child task can be executed anywhere at anytime and in parallel with its continuation.

Note

There is no guaranteed relationship between the thread that executes the lf::fork and the thread(s) that execute the continuation/child. However, currently libfork uses continuation stealing so the thread that calls lf::fork will immediately begin executing the child.

constexpr auto lf::core::call = dispatch<tag::call>

A second-order functor used to produce an awaitable (in an lf::task) that will trigger a call.

Conceptually the called/child task can be executed anywhere at anytime but, its continuation is guaranteed to be sequenced after the child returns.

Note

There is no relationship between the thread that executes the lf::call and the thread(s) that execute the continuation/child. However, currently libfork uses continuation stealing so the thread that calls lf::call will immediately begin executing the child.

constexpr impl::join_type lf::core::join = {}

An awaitable (in a lf::task) that triggers a join.

After a join is resumed it is guaranteed that all forked child tasks will have completed.

Note

There is no relationship between the thread that executes the lf::join and the thread that resumes the coroutine.

Invocation

constexpr impl::bind_just lf::core::just = {}

A second-order functor, produces an awaitable (in an lf::task) that will trigger a call + join.

Explicit

template<scheduler Sch>
auto lf::core::resume_on(Sch *dest) noexcept -> resume_on_quasi_awaitable<Sch>

Create an lf::core::context_switcher to explicitly transfer execution to dest.

dest must be non-null.

template<scheduler Sch>
struct resume_on_quasi_awaitable

An lf::core::context_switcher that just transfers execution to a new scheduler.

Advanced/generic

template<tag Tag, modifier_for<Tag> Mod = modifier::none>
constexpr auto lf::core::dispatch = impl::bind_task<Tag, Mod>{}

A second-order function for advanced control of fork/call.

Users should prefer lf::core::fork and lf::core::call over this function. lf::core::dispatch primarily caters for niche exception handling use-cases and has more stringent requirements on when/where it can be used.

If Tag == lf::core::tag::call then dispatches like lf::core::call, i.e. the parent cannot be stolen. If Tag == lf::core::tag::fork then dispatches like lf::core::fork, i.e. the parent can be stolen.

The modifiers perform the following actions:

  • lf::core::modifier::none - No modification to the call category.

  • lf::core::modifier::sync - The tag is fork, but the awaitable reports if the call was synchronous, if the call was synchronous then this fork does not count as opening a fork-join scope and the internal exception will be checked, if it was set (either by the child of a sibling) then either that exception will be rethrown or a new exception will be thrown. In either case this does not count as a join. If this is inside a fork-join scope the thrown exception must be caught and a call to co_await lf::join must be made.

  • lf::core::modifier::sync_outside - Same as sync but guarantees that the fork statement is outside a fork-join scope. Hence, if the the call completes synchronously, the exception of the forked child will be rethrown and a fork-join scope will not have been opened (hence a join is not required).

  • lf::core::modifier::eager_throw - The tag is call after resuming the awaitable the internal exception is checked, if it is set (either from the child or by a sibling) then it or a new exception will be (re)thrown.

  • lf::core::modifier::eager_throw_outside - Same as eager_throw but guarantees that the call statement is outside a fork-join scope hence, the child’s exception will be rethrown.

Template Parameters:
  • Tag – The tag of the dispatched task.

  • Mod – A modifier for the dispatched sequence.

Classes

Task

template<returnable T = void>
struct task : public std::type_identity<void>, public impl::unique_frame

The return type for libfork’s async functions/coroutines.

This predominantly exists to disambiguate libforks coroutines from other coroutines and specify T the async function’s return type which is required to be void, a reference, or a std::movable type.

Note

No consumer of this library should ever touch an instance of this type, it is used for specifying the return type of an async function only.

Warning

The value type T of a coroutine should be independent of the coroutines first-argument.

Future

template<returnable R>
class future

A future is a handle to the result of an asynchronous operation.

Public Functions

future(future &&other) noexcept = default

Move construct a new future.

future(future const &other) = delete

Futures are not copyable.

inline ~future() noexcept

Wait (block) until the future completes if it has a shared state.

inline void detach() noexcept

Detach the shared state from this future.

Following this operation the destructor is guaranteed to not block.

inline auto get() -> R

Wait (block) for the result to complete and then return it.

If the task completed with an exception then that exception will be rethrown. If the future has no shared state then a lf::core::future_error will be thrown.

auto operator=(future &&other) noexcept -> future& = default

Move assign to a future.

auto operator=(future const &other) -> future& = delete

Futures are not copy assignable.

inline auto valid() const noexcept -> bool

Test if the future has a shared state.

inline void wait()

Wait (block) for the future to complete.

Eventually

template<returnable T>
using lf::core::eventually = basic_eventually<T, false>

An alias for lf::core::basic_eventually<T, false>.

template<returnable T>
using lf::core::try_eventually = basic_eventually<T, true>

An alias for lf::core::basic_eventually<T, true>.

Defer

template<class F>
class defer : private lf::impl::immovable<defer<F>>

Basic implementation of a Golang-like defer.

Use like:

auto * ptr = c_api_init();

defer _ = [&ptr] () noexcept {
  c_api_clean_up(ptr);
};

// Code that may throw

You can also use the LF_DEFER macro to create an automatically named defer object.

Public Functions

inline constexpr defer(F &&f) noexcept(std::is_nothrow_constructible_v<F, F&&>)

Construct a new Defer object.

Parameters:

f – Nullary invocable forwarded into object and invoked by destructor.

inline constexpr ~defer() noexcept

Calls the invocable.

Exceptions

struct exception_before_join : public std::exception

Thrown when a parent knows a child threw an exception but before a join point has been reached.

This exception must be caught and then join must be called, which will rethrow the child’s exception.

Public Functions

inline auto what() const noexcept -> char const* override

A diagnostic message.

struct schedule_in_worker : public std::exception

Thrown when a worker thread attempts to call lf::core::schedule.

Public Functions

inline auto what() const noexcept -> char const* override

A diagnostic message.

struct broken_future : public std::exception

Thrown when a future has no shared state.

Public Functions

inline auto what() const noexcept -> char const* override

A diagnostic message.

struct empty_future : public std::exception

Thrown when .get() is called more than once on a future.

Public Functions

inline auto what() const noexcept -> char const* override

A diagnostic message.

Stack allocation

Functions

template<co_allocable T>
auto lf::core::co_new(std::size_t count) -> impl::co_new_t<T>

A function which returns an awaitable which triggers allocation on a worker’s stack.

Upon co_awaiting the result of this function an lf::stack_allocated object is returned.

Warning

This must be called __outside__ of a fork-join scope and is an expert only feature!

Classes

template<co_allocable T>
class stack_allocated : private lf::impl::immovable<stack_allocated<T>>

The result of co_awaiting the result of lf::core::co_new.

A raii wrapper around a std::span pointing to the memory allocated on the stack. This type can be destructured into a std::span to the allocated memory.

Public Functions

inline stack_allocated(impl::frame *frame, std::span<T> span) noexcept

Construct a new co allocated object.

inline ~stack_allocated() noexcept

Destroys objects and releases the memory.

template<std::size_t I>
inline auto get() const noexcept -> std::span<T const>

Get a span over the allocated memory.

template<std::size_t I>
inline auto get() noexcept -> std::span<T>

Get a span over the allocated memory.

Forward declarations

LF_FWD_DECL(R, f, ...)

Forward declare an async function.

This is useful for speeding up compile times by allowing you to put the definition of an async function in a source file and only forward declare it in a header file.

Usage: in a header file (e.g. fib.hpp), forward declare an async function:

LF_FWD_DECL(int, fib, int n);

And in a corresponding source file (e.g. fib.cpp), implement the function:

LF_IMPLEMENT(int, fib, int n) {

  if (n < 2) {
    co_return n;
  }

  int a, b;

  co_await lf::fork(&a, fib)(n - 1);
  co_await lf::call(&b, fib)(n - 2);

  co_await lf::join;
}

Now in some other file you can include fib.hpp and use fib as normal with the restriction that you must always bind the result of the extern’ed function to an lf::core::eventually<int> *, lf::core::try_eventually<int> * or int *.

LF_IMPLEMENT(R, f, ...)

See LF_FWD_DECL for usage.

LF_IMPLEMENT_NAMED(R, f, self, ...)

An alternative to LF_IMPLEMENT that allows you to name the self parameter.