document coroutines

master
Daniel Kolesa 2017-03-28 22:21:38 +02:00
parent 94915ee5fc
commit 049c4b8c47
1 changed files with 393 additions and 9 deletions

View File

@ -1,6 +1,20 @@
/* Coroutines for OctaSTD.
/** @addtogroup Concurrency
* @{
*/
/** @file coroutine.hh
*
* This file is part of OctaSTD. See COPYING.md for further information.
* @brief Lightweight cooperative threads.
*
* This file implements coroutines (sometimes also called fibers or green
* threads), which are essentially very lightweight cooperative threads.
* They're designed with very low context switch overhead in mind, so that
* they can be spawned in large numbers. Coupled with a scheduler, it's
* possible to create scalable concurrent/parallel applications. They can
* also be used as generators and generally anywhere where you'd like to
* suspend a function and resume it later (all kinds of async apps).
*
* @copyright See COPYING.md in the project tree for further information.
*/
#ifndef OSTD_COROUTINE_HH
@ -22,6 +36,14 @@
namespace ostd {
/** @addtogroup Concurrency
* @{
*/
/** @brief Thrown on coroutine related errors.
*
* These can include a dead coroutine/generator call/access.
*/
struct coroutine_error: std::runtime_error {
using std::runtime_error::runtime_error;
};
@ -55,21 +77,53 @@ namespace detail {
OSTD_EXPORT extern thread_local coroutine_context *coro_current;
} /* namespace detail */
/** @brief An encapsulated context any coroutine-type inherits from.
*
* Internally, this provides some basic infrastructure for creating and
* managing the coroutine as well as its entry point. All coroutine types
* inherit from this.
*/
struct coroutine_context {
/** @brief Gets the currently executing coroutine context.
*
* Sometimes custom coroutine types might want to bypass being able
* to be accessed through this, so they can choose to avoid calling
* call(). The coroutine-based concurrent schedulers particularly do
* this. This means that if you call this while in a task, it will
* return either `nullptr` or the non-task coroutine we're currently in.
*
* @returns The current context.
*/
static coroutine_context *current() {
return detail::coro_current;
}
protected:
/** Does nothing, just default-initializes the members. */
coroutine_context() {}
/* coroutine context must be polymorphic */
/** @brief Unwinds the stack and frees it.
*
* If the coroutine is dead or just initialized and was never called,
* no unwind is performed (in the former case, it's already done, in
* the latter case, it was never needed in the first place). Then the
* stack is freed (if present) using the same stack allocator it was
* allocated with.
*/
virtual ~coroutine_context() {
unwind();
free_stack();
}
/** Coroutine contexts are not copy constructible. */
coroutine_context(coroutine_context const &) = delete;
/** @brief Moves the given context into this one.
*
* The other context is deinitialized and set as dead.
*
* @see operator=(coroutine_context &&)
*/
coroutine_context(coroutine_context &&c):
p_stack(std::move(c.p_stack)), p_coro(c.p_coro), p_orig(c.p_orig),
p_except(std::move(c.p_except)), p_state(c.p_state), p_free(c.p_free)
@ -80,12 +134,33 @@ protected:
c.set_dead();
}
/** Coroutine contexts are not copy assignable. */
coroutine_context &operator=(coroutine_context const &) = delete;
/** @brief Moves the given context into this one.
*
* Effectively calls swap(coroutine_context &). The current context
* is then destroyed by the other context's destructor.
*
* @returns Returns `*this*`.
*/
coroutine_context &operator=(coroutine_context &&c) {
swap(c);
return *this;
}
/** @brief Calls the coroutine context.
*
* Sets the state flag as executing, saves the current context and
* sets it to this coroutine (so that current() returns this context)
* and jumps into the coroutine function. Once the coroutine yields or
* returns, sets the current context back to what was saved previously.
*
* Additionally, propagates any uncaught exception that was thrown inside
* of the coroutine function by calling rethrow().
*
* @throws Whatever the coroutine function throws.
*/
void call() {
set_exec();
/* switch to new coroutine */
@ -96,42 +171,73 @@ protected:
rethrow();
}
/** @brief Jumps into the coroutine context.
*
* Only valid if the coroutine is currently suspended or not started.
*
* @see yield_jump(), yield_done()
*/
void coro_jump() {
p_coro = detail::ostd_jump_fcontext(p_coro, this).ctx;
}
/** @brief Jumps out of the coroutine context.
*
* Only valid if the coroutine is currently running. Sets the state
* to suspended before jumping.
*
* @see coro_jump(), yield_done()
*/
void yield_jump() {
p_state = state::HOLD;
p_orig = detail::ostd_jump_fcontext(p_orig, nullptr).ctx;
}
/** @brief Jumps out of the coroutine context.
*
* Only valid if the coroutine is currently running. Sets the state
* to dead before jumping.
*
* @see coro_jump(), yield_jump()
*/
void yield_done() {
set_dead();
p_orig = detail::ostd_jump_fcontext(p_orig, nullptr).ctx;
}
/** Checks if the coroutine is suspended. */
bool is_hold() const {
return (p_state == state::HOLD);
}
/** Checks if the coroutine is dead. */
bool is_dead() const {
return (p_state == state::TERM);
}
/** Marks the coroutine as dead. Only valid without an active context. */
void set_dead() {
p_state = state::TERM;
}
/** Marks the coroutine as executing. Only valid before coro_jump(). */
void set_exec() {
p_state = state::EXEC;
}
/** @brief Rethrows an exception saved by a previous coroutine call.
*
* Call this after coro_jump() when implementing your own call().
* When not implementing your own call(), do not do this anywhere,
* the exception is already rethrown inside call().
*/
void rethrow() {
if (p_except) {
std::rethrow_exception(std::move(p_except));
}
}
/** Swaps the current context with another. */
void swap(coroutine_context &other) noexcept {
using std::swap;
swap(p_stack, other.p_stack);
@ -142,6 +248,23 @@ protected:
swap(p_free, other.p_free);
}
/** @brief Allocates a stack and creates a context.
*
* Only call this once at the point where no context was created yet.
* That is typically in a constructor that results in a valid coroutine.
*
* The context uses an internal entry point. The entry point calls the
* coroutine's function by calling the `resume_call()` method with no
* parameters and results on it. Any exception thrown from it is caught
* and saved as an `std::exception_ptr`. This is then rethrown by a call
* to rethrow().
*
* It's therefore necessary for @p C to give the context access to its
* `resume_call()` method, typically by making coroutine_context a friend.
*
* @param sa The stack allocator used to allocate the stack.
* @tparam C The coroutine type that inherits from the context class.
*/
template<typename C, typename SA>
void make_context(SA &sa) {
p_stack = sa.allocate();
@ -248,6 +371,11 @@ release:
void (*p_free)(void *) = nullptr;
};
/** @brief Unspecialized declaration of the coroutine.
*
* This one has no definition. All instances of coroutine must use `R(A...)`
* as template arguments, which is specialized later.
*/
template<typename T>
struct coroutine;
@ -482,6 +610,48 @@ namespace detail {
};
} /* namespace detail */
/** @brief The coroutine type specialization.
*
* This is a regular coroutine. Depending on the template param, it can
* have a return type as well as arguments. The typical usage is as follows:
*
* ~~~{.cc}
* coroutine<int(float, bool)> x = [](auto yield, float a, bool b) {
* writefln("call 1: %s %s", a, b);
* std::tie(a, b) = yield(5); // "returns" 5
* writefln("call 2: %s %s", a, b);
* return 10;
* };
*
* int r = x(3.14f, true); // call 1: 3.14 true
* writeln(r); // 5
*
* r = x(6.28f, false); // call 2: 6.28 false
* writeln(r); // 10, x is dead here
* ~~~
*
* As you can see, the coroutine function takes an object by value (yield_type)
* and any declared arguments. Any types can be arguments as well as returns,
* just like with any function or functional wrapper like `std::function`.
*
* You suspend the coroutine by calling its yield type. Depending on the
* result type, the yielder call takes an argument or not. If `R` is `void`,
* the yielder takes no argument. If it's a non-reference type, the yielder
* takes an argument by lvalue reference to `const` (`R const &`) or by rvalue
* reference (`R &&`). If it's an lvalue reference type, it takes the lvalue
* reference type. If it's an rvalue reference type, it takes the rvalue
* reference.
*
* The yielder returns a value or not depending on the argument types. With
* no argument types, nothing is returned. With a single argument type, the
* argument itself is returned. With two argument types, an `std::pair` of
* the arguments is returned. Otherwise, an `std::tuple` of the arguments
* is returned. The arguments are never copied or moved, you get them exactly
* as they were passed, exactly as if they were passed as regular args.
*
* @tparam R The return template type.
* @tparam A... The argument template types.
*/
template<typename R, typename ...A>
struct coroutine<R(A...)>: coroutine_context {
private:
@ -555,12 +725,25 @@ private:
};
public:
/** The yielder type for the coroutine. Not opaque, but internal. */
using yield_type = yielder<R, A...>;
/* we have no way to assign a function anyway... */
/** Coroutines are not default constructible. */
coroutine() = delete;
/* 0 means default size decided by the stack allocator */
/** @brief Creates a coroutine using the given function.
*
* The function is stored in an appropriate `std::function`. If the
* resulting function is empty, it's the same as initializing the
* coroutine with a null pointer (i.e. it's dead by default).
*
* Otherwise creates a context using the provided stack allocator.
*
* @param func The function to use.
* @param sa The stack allocator, defaults to a default_stack.
*
* @throws Whatever an `std::function` constructor could throw.
*/
template<typename F, typename SA = default_stack>
coroutine(F func, SA sa = SA{}): base_t(), p_stor(std::move(func)) {
/* that way there is no context creation/stack allocation */
@ -571,29 +754,60 @@ public:
this->make_context<coroutine<R(A...)>>(sa);
}
/** @brief Creates a dead coroutine.
*
* No context is created. No stack is allocated.
*/
template<typename SA = default_stack>
coroutine(std::nullptr_t, SA = SA{}): base_t(), p_stor() {
this->set_dead();
}
/** Coroutines are not copy constructible. */
coroutine(coroutine const &) = delete;
/** @brief Moves a coroutine.
*
* Moves the given coroutine's context and any other data into the new
* coroutine. The other coroutine is left in a dead state.
*
* @see operator=(coroutine &&)
*/
coroutine(coroutine &&c) noexcept:
base_t(std::move(c)), p_stor(std::move(c.p_stor))
{
c.p_stor.p_func = nullptr;
}
/** Coroutines are not copy assignable. */
coroutine &operator=(coroutine const &) = delete;
/** @brief Moves a coroutine.
*
* Effectively like a call to swap(coroutine &).
*/
coroutine &operator=(coroutine &&c) noexcept {
base_t::operator=(std::move(c));
p_stor = std::move(c.p_stor);
c.p_stor.p_func = nullptr;
p_stor.swap(c.p_stor);
}
/** Checks if the coroutine is alive, returning `true` if it is. */
explicit operator bool() const noexcept {
return !this->is_dead();
}
/** @brief Calls the coroutine.
*
* Executes the coroutine with the given arguments and returns whatever
* the coroutine returns or yields. The arguments are never copied or
* moved, they're passed into the coroutine exactly as real args would.
*
* @returns The value passed to `yield()` (or nothing if `R` is `void`).
*
* @throws ostd::coroutine_error if the coroutine is dead.
*
* @see operator()(A...)
*/
R resume(A ...args) {
if (this->is_dead()) {
throw coroutine_error{"dead coroutine"};
@ -603,6 +817,12 @@ public:
return this->get_result();
}
/** @brief Calls the coroutine.
*
* Exactly the same as calling resume(A...), with function syntax.
*
* @see resume(A...)
*/
R operator()(A ...args) {
/* duplicate the logic so we don't copy/move the args */
if (this->is_dead()) {
@ -613,18 +833,30 @@ public:
return p_stor.get_result();
}
/** Swaps two coroutines' states. */
void swap(coroutine &other) noexcept {
p_stor.swap(other.p_stor);
base_t::swap(other);
}
/** Returns the RTTI of the function stored in the coroutine. */
std::type_info const &target_type() const {
return p_stor.p_func.target_type();
}
/** @brief Retrieves the stored function given its known type.
*
* @returns A pointer to the function or `nullptr` if the type
* doesn't match.
*/
template<typename F>
F *target() { return p_stor.p_func.target(); }
/** @brief Retrieves the stored function given its known type.
*
* @returns A pointer to the function or `nullptr` if the type
* doesn't match.
*/
template<typename F>
F const *target() const { return p_stor.p_func.target(); }
@ -636,6 +868,7 @@ private:
detail::coro_stor<yield_type, R, A...> p_stor;
};
/** Swaps two coroutines' states. */
template<typename R, typename ...A>
inline void swap(coroutine<R(A...)> &a, coroutine<R(A...)> &b) noexcept {
a.swap(b);
@ -647,6 +880,42 @@ namespace detail {
template<typename T> struct generator_iterator;
}
/** @brief A generator type.
*
* Generators are much like coroutines and are also backed by contexts and
* stacks. However, there are a few important differences:
*
* * Generators take no arguments (besides the yielder).
* * Generators don't return from their function, but pass values to `yield()`.
* * Generators store a value until resumed, which can be checked many times.
* * Generators are started automatically and have a value until they die.
* * Generators are iterable and you can even take ranges to them.
*
* This is best described with an example:
*
* ~~~{.cc}
* auto x = [](auto yield) {
* for (int i = 1; i <= 5; ++i) {
* yield(i * 5);
* }
* };
*
* for (int i: generator<int>{x}) {
* writeln(i); // 5, 10, 15, 20, 25
* }
*
* generator<int> y = x;
* writeln(y.value()); // 5
* writeln(y.value()); // 5
* y.resume();
* writeln(y.value()); // 10
* ~~~
*
* As you can see, where coroutines are basically just resumable functions,
* generators are truly suitable for generating values.
*
* @tparam T The value type to use.
*/
template<typename T>
struct generator: coroutine_context {
private:
@ -701,12 +970,29 @@ private:
};
public:
/** As generators can be used with ranges, this is defined. */
using range = generator_range<T>;
/** The yielder type for the generator. Not opaque, but internal. */
using yield_type = yielder<T>;
/** Generators are not default constructible. */
generator() = delete;
/** @brief Creates a generator using the given function.
*
* The function is stored in an appropriate `std::function`. If the
* resulting function is empty, it's the same as initializing the
* generator with a null pointer (i.e. it's dead by default).
*
* Otherwise creates a context using the provided stack allocator
* and then resumes the generator, making it get a value (or die).
*
* @param func The function to use.
* @param sa The stack allocator, defaults to a default_stack.
*
* @throws Whatever an `std::function` constructor could throw.
*/
template<typename F, typename SA = default_stack>
generator(F func, SA sa = SA{}):
base_t(), p_func(std::move(func))
@ -721,6 +1007,10 @@ public:
resume();
}
/** @brief Creates a dead generator.
*
* No context is created. No stack is allocated.
*/
template<typename SA = default_stack>
generator(std::nullptr_t, SA = SA{0}):
base_t(), p_func(nullptr)
@ -728,7 +1018,16 @@ public:
this->set_dead();
}
/** Generators are not copy constructible. */
generator(generator const &) = delete;
/** @brief Moves a generator.
*
* Moves the given generator's context and any other data into the new
* generator. The other generator is left in a dead state.
*
* @see operator=(generator &&)
*/
generator(generator &&c) noexcept:
base_t(std::move(c)), p_func(std::move(c.p_func)), p_result(c.p_result)
{
@ -736,7 +1035,13 @@ public:
c.p_result = nullptr;
}
/** Generators are not copy assignable. */
generator &operator=(generator const &) = delete;
/** @brief Moves a generator.
*
* Effectively like a call to swap(generator &).
*/
generator &operator=(generator &&c) noexcept {
base_t::operator=(std::move(c));
p_func = std::move(c.p_func);
@ -745,10 +1050,19 @@ public:
c.p_result = nullptr;
}
/** Checks if the generator is alive, returning `true` if it is. */
explicit operator bool() const noexcept {
return !this->is_dead();
}
/** @brief Resumes the generator, going to the next value (or dying).
*
* Executes the generator and if it yields, the yielded value will then
* be accessible via value(). Otherwise, the generator dies and there
* will not be any value.
*
* @throws ostd::coroutine_error if the generator is dead.
*/
void resume() {
if (this->is_dead()) {
throw coroutine_error{"dead generator"};
@ -756,6 +1070,14 @@ public:
coroutine_context::call();
}
/** @brief Retrieves a reference to the generator's value.
*
* When the generator is in a suspended state, the value it has yielded
* lies on it stack, which is guaranteed to be alive. Therefore we don't
* have to copy the value and can return a cheap reference to it.
*
* @throws ostd::coroutine_error if the generator is dead.
*/
T &value() {
if (!p_result) {
throw coroutine_error{"no value"};
@ -763,6 +1085,14 @@ public:
return *p_result;
}
/** @brief Retrieves a reference to the generator's value.
*
* When the generator is in a suspended state, the value it has yielded
* lies on it stack, which is guaranteed to be alive. Therefore we don't
* have to copy the value and can return a cheap reference to it.
*
* @throws ostd::coroutine_error if the generator is dead.
*/
T const &value() const {
if (!p_result) {
throw coroutine_error{"no value"};
@ -770,16 +1100,30 @@ public:
return *p_result;
}
/** Checks if the generator is has a value, returning `false` if it does. */
bool empty() const noexcept {
return !p_result;
}
/** @brief Gets a range to the generator.
*
* The range is an ostd::generator_range<T>. It's an input range, so all
* of its copies point to the same generator instance, reflecting the same
* internal state.
*/
generator_range<T> iter() noexcept;
/* for range for loop; they're the same, operator!= bypasses comparing */
/** Implements a minimal iterator just for range-based for loop.
* Do not use directly.
*/
detail::generator_iterator<T> begin() noexcept;
/** Implements a minimal iterator just for range-based for loop.
* Do not use directly.
*/
detail::generator_iterator<T> end() noexcept;
/** Swaps two generators' states. */
void swap(generator &other) noexcept {
using std::swap;
swap(p_func, other.p_func);
@ -787,13 +1131,24 @@ public:
coroutine_context::swap(other);
}
/** Returns the RTTI of the function stored in the generator. */
std::type_info const &target_type() const {
return p_func.target_type();
}
/** @brief Retrieves the stored function given its known type.
*
* @returns A pointer to the function or `nullptr` if the type
* doesn't match.
*/
template<typename F>
F *target() { return p_func.target(); }
/** @brief Retrieves the stored function given its known type.
*
* @returns A pointer to the function or `nullptr` if the type
* doesn't match.
*/
template<typename F>
F const *target() const { return p_func.target(); }
@ -811,6 +1166,7 @@ private:
std::remove_reference_t<T> *p_result = nullptr;
};
/** Swaps two generators' states. */
template<typename T>
inline void swap(generator<T> &a, generator<T> &b) noexcept {
a.swap(b);
@ -827,9 +1183,20 @@ namespace detail {
};
}
/** @brief Gets the yielder type for the given type.
*
* If the given type is like `R(A...)`, the resulting type is an yielder type
* for the matching ostd::coroutine. If it's just a simple type, it's an
* yielder type for a generator of that type.
*/
template<typename T>
using yield_type = typename detail::yield_type_base<T>::type;
/** @brief A range type over a generator.
*
* This range type is a simple input range type (ostd::input_range_tag)
* that wraps around a generator.
*/
template<typename T>
struct generator_range: input_range<generator_range<T>> {
using range_category = input_range_tag;
@ -838,27 +1205,40 @@ struct generator_range: input_range<generator_range<T>> {
using size_type = size_t;
using difference_type = ptrdiff_t;
/** Generator ranges are not default constructible. */
generator_range() = delete;
/** Generator ranges are constructed using a reference to a generator. */
generator_range(generator<T> &g): p_gen(&g) {}
/** Like ostd::generator<T>::empty(). */
bool empty() const noexcept {
return p_gen->empty();
}
/** Like ostd::generator<T>::resume(). */
void pop_front() {
p_gen->resume();
}
/** Like ostd::generator<T>::value(). */
reference front() const {
return p_gen->value();
}
/** Simply compares the two stored generator references for equality. */
bool equals_front(generator_range const &g) const noexcept {
return p_gen == g.p_gen;
}
/* same behavior as on generator itself, for range for loop */
/** Implements a minimal iterator just for range-based for loop.
* Do not use directly.
*/
detail::generator_iterator<T> begin() noexcept;
/** Implements a minimal iterator just for range-based for loop.
* Do not use directly.
*/
detail::generator_iterator<T> end() noexcept;
private:
@ -915,6 +1295,10 @@ detail::generator_iterator<T> generator_range<T>::end() noexcept {
return detail::generator_iterator<T>{*p_gen};
}
/** @} */
} /* namespace ostd */
#endif
/** @} */