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>.
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
cvrefqualifiers.
-
template<typename T>
concept returnable¶ - #include <libfork/core/task.hpp>
A type returnable from libfork’s async functions/coroutines.
This requires that
Tisvoida reference or astd::movabletype.
-
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 islf::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::taskwhenoperator()is called. with appropriate arguments. The call tooperator()must create a libfork coroutine. The first argument of an async function must accept a deduced templated-type that satisfies thelf::core::first_argconcept. 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
taggedto obtain this value. Additionally, a user may query the first argument’s static member functioncontext()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,
schedulewhich accepts anlf::submit_handleand promises to calllf::resume()on it. Theschedulemethod 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_switchercan 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’sawait_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 andnoexceptandptris 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 tostash_exception.
Functional¶
-
template<typename I, tag Tag, typename F, typename ...Args>
concept async_tag_invocable¶ - #include <libfork/core/invocable.hpp>
Check
FisTag-invocable withArgs...and returns anlf::taskwho’s result is returnable viaI.In the following description “invoking” or “async invoking” means to call
FwithArgs...via the appropriate libfork function i.e.forkcorresponds tolf::fork[r, f](args...)and the library will generate the appropriate (opaque) first-argument.This requires:
Fis an async function object.Fis ‘Tag’/call invocable withArgs...when writing the result toIor 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>.Iis movable and dereferenceable.Iis indirectly writable fromRorRisvoidwhileIisdiscard_t.If
Ris non-void thenFislf::core::async_tag_invocablewhenIislf::eventually<R> *.Fislf::core::async_tag_invocablewhenIislf::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::forkableandlf::core::rootable, subsumes both.
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
funonschand return alf::core::futureto the result.This will build a task from
funand dispatch it toschvia itsschedulemethod. Ifscheduleis called by a worker thread (which are never allowed to block) thenlf::core::schedule_in_workerwill 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
funonschand 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
maininto a scheduler/runtime by scheduling a single root-task with this function.This makes the appropriate call to
lf::core::scheduleand callsgeton the returnedlf::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
funonschand detach the future.This is the secondary entry point from the synchronous to the asynchronous world. Similar to
sync_waitbut callsdetachon the returnedlf::core::future.Note: Many schedulers (like
lf::lazy_poolandlf::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::forkand the thread(s) that execute the continuation/child. However, currentlylibforkuses continuation stealing so the thread that callslf::forkwill 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::calland the thread(s) that execute the continuation/child. However, currentlylibforkuses continuation stealing so the thread that callslf::callwill 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::joinand the thread that resumes the coroutine.
Invocation¶
Explicit¶
-
template<scheduler Sch>
auto lf::core::resume_on(Sch *dest) noexcept -> resume_on_quasi_awaitable<Sch>¶ Create an
lf::core::context_switcherto explicitly transfer execution todest.destmust be non-null.
-
template<scheduler Sch>
struct resume_on_quasi_awaitable¶ An
lf::core::context_switcherthat 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::forkandlf::core::callover this function.lf::core::dispatchprimarily caters for niche exception handling use-cases and has more stringent requirements on when/where it can be used.If
Tag == lf::core::tag::callthen dispatches likelf::core::call, i.e. the parent cannot be stolen. IfTag == lf::core::tag::forkthen dispatches likelf::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 isfork, 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 toco_await lf::joinmust be made.lf::core::modifier::sync_outside- Same assyncbut 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 iscallafter 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 aseager_throwbut 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 specifyTthe async function’s return type which is required to bevoid, a reference, or astd::movabletype.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
Tof 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
-
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_errorwill be thrown.
-
inline auto valid() const noexcept -> bool¶
Test if the future has a shared state.
-
inline void wait()¶
Wait (block) for the future to complete.
-
inline ~future() noexcept¶
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_DEFERmacro to create an automatically named defer object.
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.
-
inline auto what() const noexcept -> char const* override¶
-
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.
-
inline auto what() const noexcept -> char const* override¶
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 anlf::stack_allocatedobject 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 oflf::core::co_new.A raii wrapper around a
std::spanpointing to the memory allocated on the stack. This type can be destructured into astd::spanto 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.
-
inline stack_allocated(impl::frame *frame, std::span<T> span) noexcept¶
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.hppand usefibas normal with the restriction that you must always bind the result of the extern’ed function to anlf::core::eventually<int> *,lf::core::try_eventually<int> *orint *.
-
LF_IMPLEMENT(R, f, ...)¶
See
LF_FWD_DECLfor usage.
-
LF_IMPLEMENT_NAMED(R, f, self, ...)¶
An alternative to
LF_IMPLEMENTthat allows you to name theselfparameter.