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
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
isvoid
a reference or astd::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 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::task
whenoperator()
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_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 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,
schedule
which accepts anlf::submit_handle
and promises to calllf::resume()
on it. Theschedule
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’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 andnoexcept
andptr
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 tostash_exception
.
Functional¶
-
template<typename I, tag Tag, typename F, typename ...Args>
concept async_tag_invocable¶ - #include <libfork/core/invocable.hpp>
Check
F
isTag
-invocable withArgs...
and returns anlf::task
who’s result is returnable viaI
.In the following description “invoking” or “async invoking” means to call
F
withArgs...
via the appropriate libfork function i.e.fork
corresponds tolf::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 withArgs...
when writing the result toI
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 fromR
orR
isvoid
whileI
isdiscard_t
.If
R
is non-void thenF
islf::core::async_tag_invocable
whenI
islf::eventually<R> *
.F
islf::core::async_tag_invocable
whenI
islf::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
andlf::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
fun
onsch
and return alf::core::future
to the result.This will build a task from
fun
and dispatch it tosch
via itsschedule
method. Ifschedule
is called by a worker thread (which are never allowed to block) thenlf::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
onsch
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 callsget
on 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
fun
onsch
and detach the future.This is the secondary entry point from the synchronous to the asynchronous world. Similar to
sync_wait
but callsdetach
on the returnedlf::core::future
.Note: Many schedulers (like
lf::lazy_pool
andlf::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, currentlylibfork
uses continuation stealing so the thread that callslf::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, currentlylibfork
uses continuation stealing so the thread that callslf::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¶
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 todest
.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
andlf::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 likelf::core::call
, i.e. the parent cannot be stolen. IfTag == lf::core::tag::fork
then 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::join
must be made.lf::core::modifier::sync_outside
- Same assync
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 iscall
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 aseager_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
libfork
s coroutines from other coroutines and specifyT
the async function’s return type which is required to bevoid
, a reference, or astd::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
-
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.
-
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_DEFER
macro 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_await
ing the result of this function anlf::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_await
ing the result oflf::core::co_new
.A raii wrapper around a
std::span
pointing to the memory allocated on the stack. This type can be destructured into astd::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.
-
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.hpp
and usefib
as 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_DECL
for usage.
-
LF_IMPLEMENT_NAMED(R, f, self, ...)¶
An alternative to
LF_IMPLEMENT
that allows you to name theself
parameter.