Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ header only core
## cth::coro
coroutine runtime on asio

- `co::context`: interface of where coroutines run and wait, implemented by `scheduler` and `strand`
- `co::scheduler`: `asio::io_context` on its own threads
- `co::executor`: spawns tasks, moves execution back onto the scheduler after foreign awaitables
- `co::strand`: serial queue on a scheduler, its jobs run one at a time and in post order
- `co::executor`: spawns tasks, moves execution back onto the context after foreign awaitables
- `this_coro::executor`, `this_coro::wait(duration)`, native handle waits

## cth::os
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
#pragma once
#include "cth/coro/scheduler.hpp"
#include "cth/coro/context.hpp"
#include "cth/coro/tasks/awaiters/dev/this_coro_awaiter_base.hpp"

#include <cth/os/wait.hpp>


namespace cth::co {
template<this_coro::scheduler_payload Pyld = this_coro::default_payload>
template<this_coro::context_payload Pyld = this_coro::default_payload>
struct native_handle_awaiter_base : dev::this_coro_awaiter_base<Pyld> {
constexpr explicit native_handle_awaiter_base(os::native_handle_t handle) : _handle{handle} {}

[[nodiscard]] bool await_ready() const { return os::check_ready(_handle); }
void await_suspend(std::coroutine_handle<> h) const {
this->payload().scheduler().await(_handle, [h] { h.resume(); });
this->payload().context().await(_handle, [h] { h.resume(); });
}

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include "cth/os/fence.hpp"

namespace cth::co {
template<this_coro::scheduler_payload Pyld = this_coro::default_payload>
template<this_coro::context_payload Pyld = this_coro::default_payload>
struct osfence_awaiter_base : native_handle_awaiter_base<Pyld> {
explicit osfence_awaiter_base(os::fence const& fence) : native_handle_awaiter_base<Pyld>{
fence.native_handle()} {}
Expand Down
6 changes: 3 additions & 3 deletions cth_coro/incl/cth/coro/awaiters/raw_native_handle_awaiter.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once

#include "cth/coro/scheduler.hpp"
#include "cth/coro/context.hpp"

#include <cth/os/wait.hpp>

Expand All @@ -9,12 +9,12 @@

namespace cth::co {
struct raw_native_handle_awaiter {
co::scheduler const& scheduler;
co::context const& context;
os::native_handle_t handle;

[[nodiscard]] bool await_ready() const { return os::check_ready(handle); }
void await_suspend(std::coroutine_handle<> h) const {
scheduler.await(handle, [h] { h.resume(); });
context.await(handle, [h] { h.resume(); });
}
static constexpr void await_resume() {}
};
Expand Down
39 changes: 26 additions & 13 deletions cth_coro/incl/cth/coro/awaiters/schedule_awaiter.hpp
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
#pragma once
#include "cth/coro/scheduler.hpp"
#include "cth/coro/context.hpp"
#include "cth/coro/this_coro/this_coro_base.hpp"
#include "cth/coro/utility/concepts.hpp"
#include "cth/coro/utility/fwd.hpp"

#include <coroutine>


namespace cth::co {
/**
* switches execution onto a target @ref scheduler and stays there
* @details
* switches execution onto a target @ref context and stays there
* @details noop if the coroutine already runs on the target: @ref context::current() is the target
* @tparam Pyld payload of the migrating coroutine, defaulted to @ref this_coro::default_payload
*/
template<this_coro::scheduler_payload Pyld>
template<this_coro::context_payload Pyld>
struct schedule_awaiter : this_coro_base<Pyld> {
public:
constexpr schedule_awaiter(co::scheduler const& target) : _target{target} {}

[[nodiscard]] bool await_ready() const noexcept { return _target.owns_thread(); }

template<promise Promise> requires this_coro::scheduler_payload<Pyld>
void await_suspend(std::coroutine_handle<Promise> h) {
constexpr schedule_awaiter(co::context const& target) : _target{target} {}

/**
* @return false, @ref await_suspend() decides
*/
[[nodiscard]] constexpr bool await_ready() const noexcept { return false; }

/**
* rebinds the payload to the target and posts the coroutine onto the target
* @details owns_thread() is not enough: a scheduler also owns the threads that run its strands
* @return false if the coroutine already runs on the target
*/
template<promise Promise> requires this_coro::context_payload<Pyld>
bool await_suspend(std::coroutine_handle<Promise> h) {
// even though we don't inject it, payload compatibility should be enforced
static_assert(!this_coro_compatible<Promise> || this_coro::promise_injectable_with<Promise, Pyld>);

if constexpr(this_coro::scheduler_payload<Pyld> && this_coro_compatible<Promise>)
h.promise().rebind_scheduler(_target);
if constexpr(has_context<Promise>)
h.promise().rebind_context(_target);

if(context::current() == &_target)
return false;

_target.post([h]() { h.resume(); });
return true;
}

template<pyld_injectable_to<Pyld> P>
Expand All @@ -36,6 +49,6 @@ struct schedule_awaiter : this_coro_base<Pyld> {
constexpr void await_resume() const noexcept {}

private:
co::scheduler const& _target;
co::context const& _target;
};
}
6 changes: 3 additions & 3 deletions cth_coro/incl/cth/coro/awaiters/wait_awaiter.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#pragma once
#include "cth/coro/scheduler.hpp"
#include "cth/coro/context.hpp"
#include "cth/coro/this_coro/payload.hpp"
#include "cth/coro/utility/fwd.hpp"

Expand All @@ -11,14 +11,14 @@ namespace cth::co {
struct wait_awaiter {

std::chrono::steady_clock::time_point timePoint;
co::scheduler const& scheduler;
co::context const& context;

[[nodiscard]] bool await_ready() const noexcept {
return std::chrono::steady_clock::now() >= timePoint;
}

void await_suspend(std::coroutine_handle<> h) const {
scheduler.await(timePoint, [h] { h.resume(); });
context.await(timePoint, [h] { h.resume(); });
}

constexpr void await_resume() const noexcept {}
Expand Down
84 changes: 84 additions & 0 deletions cth_coro/incl/cth/coro/context.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#pragma once
#include <cth/chrono.hpp>
#include <cth/os/native_handle.hpp>

#include <functional>
#include <utility>


namespace cth::co {
/**
* where coroutines run and wait
* @details implemented by @ref scheduler and @ref strand, @ref executor and the awaiters only use this interface
*/
class context {
public:
using void_func = std::move_only_function<void()>;
using native_handle = os::native_handle_t;

/**
* runs the work on this context
*/
virtual void post(void_func work) const = 0;

/**
* calls the callback on this context once the handle is signaled
* @param cb to call
*/
virtual void await(native_handle, void_func cb) const = 0;
/**
* calls the callback on this context once the time point is reached
* @param time_point to wait for
* @param cb to call
*/
virtual void await(chrono::time_point_t time_point, void_func cb) const = 0;

/**
* true if the calling thread runs a job of this context
* @details a scheduler also owns the threads that run the jobs of its strands
*/
[[nodiscard]] virtual bool owns_thread() const = 0;

/**
* @return context of the job that runs on the calling thread, nullptr -> no job of a context
* @details a strand job -> the strand, an other job of a scheduler -> the scheduler
*/
[[nodiscard]] static context const* current() noexcept { return _current; }

protected:
context() = default;
~context() = default;

context(context const& other) = default;
context& operator=(context const& other) = default;
context(context&& other) noexcept = default;
context& operator=(context&& other) noexcept = default;

/**
* wraps the work: @ref current() is this context while the work runs
* @details restores the previous context after the work, also if the work throws
*/
[[nodiscard]] auto scoped(void_func work) const {
return [self = this, work = std::move(work)]() mutable {
current_guard const guard{self};
work();
};
}

/**
* context of the job that runs on this thread, see @ref current()
*/
static inline thread_local context const* _current = nullptr;

private:
struct current_guard {
explicit current_guard(context const* ctx) noexcept : previous{std::exchange(_current, ctx)} {}
~current_guard() { _current = previous; }

context const* previous;

current_guard(current_guard const&) = delete;
current_guard& operator=(current_guard const&) = delete;
};
};
}
54 changes: 27 additions & 27 deletions cth_coro/incl/cth/coro/executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,68 +11,68 @@

namespace cth::co {
/**
* wraps the scheduler with coroutines functionality
* wraps the context with coroutines functionality
*/
class executor {
public:
constexpr executor(co::scheduler const& sched) noexcept : _sched(&sched) {}
constexpr executor(co::context const& context) noexcept : _context(&context) {}
constexpr ~executor() = default;

/**
* calling co_await on this switches the execution context to this scheduler and stays there
* calling co_await on this switches the execution onto this context and stays there
* @tparam Pyld payload the caller must carry, defaulted to @ref this_coro::default_payload
*/
template<payload Pyld = this_coro::default_payload>
[[nodiscard]] constexpr auto schedule() const { return schedule_awaiter<Pyld>{scheduler()}; }
[[nodiscard]] constexpr auto schedule() const { return schedule_awaiter<Pyld>{context()}; }


/**
* steals the execution context to the scheduler after the coroutine finished, keeps it as awaitable
* @tparam Pyld payload type; the scheduler is injected automatically when it is a scheduler_payload
* steals the execution to the context after the coroutine finished, keeps it as awaitable
* @tparam Pyld payload type; the context is injected automatically when it is a context_payload
* @tparam Awaitable must be compatible with the this_coro framework
* @param awaitable to steal context from
* @param args extra arguments to construct the payload with (besides the auto-injected scheduler)
* @param args extra arguments to construct the payload with (besides the auto-injected context)
* @return awaitable with context stolen
* @attention the awaiter does not own the task, co_await it in the same expression, do not store it
*/
template<payload Pyld = this_coro::default_payload, this_coro_awaitable Awaitable, class... Args>
[[nodiscard]] constexpr auto steal(Awaitable&& awaitable, Args&&... args) const -> awaiter_t<Awaitable> {
if constexpr(this_coro::scheduler_payload<Pyld>)
return co::steal(std::forward<Awaitable>(awaitable), Pyld{scheduler(), std::forward<Args>(args)...});
if constexpr(this_coro::context_payload<Pyld>)
return co::steal(std::forward<Awaitable>(awaitable), Pyld{context(), std::forward<Args>(args)...});
else
return co::steal(std::forward<Awaitable>(awaitable), Pyld{std::forward<Args>(args)...});
}

/**
* steals the execution context to the scheduler after the coroutine finished
* steals the execution to the context after the coroutine finished
* @note converts the awaitable to a task
* @tparam Pyld payload type; the scheduler is injected automatically when it is a scheduler_payload
* @tparam Pyld payload type; the context is injected automatically when it is a context_payload
* @tparam Awaitable must not be compatible with the this_coro framework
* @param awaitable to steal context from
* @param args extra arguments to construct the payload with (besides the auto-injected scheduler)
* @param args extra arguments to construct the payload with (besides the auto-injected context)
* @return task with context stolen
*/
template<payload Pyld = this_coro::default_payload, non_this_coro_awaitable Awaitable, class... Args>
[[nodiscard]] auto steal(Awaitable awaitable, Args&&... args) const -> capture_task<awaited_t<Awaitable>> {
if constexpr(this_coro::scheduler_payload<Pyld>)
return co::steal(std::move(awaitable), Pyld{scheduler(), std::forward<Args>(args)...});
if constexpr(this_coro::context_payload<Pyld>)
return co::steal(std::move(awaitable), Pyld{context(), std::forward<Args>(args)...});
else
return co::steal(std::move(awaitable), Pyld{std::forward<Args>(args)...});
}

/**
* runs the coroutine on the scheduler once started
* @details the caller continues on its own scheduler afterwards, with one post if the coroutine ended elsewhere,
* runs the coroutine on the context once started
* @details the caller continues on its own context afterwards, with one post if the coroutine ended elsewhere,
* a caller without this_coro support continues where the coroutine ended
* @tparam Pyld payload type; the scheduler is injected automatically when it is a scheduler_payload
* @param task coroutine to run on the scheduler
* @param args extra arguments to construct the payload with (besides the auto-injected scheduler)
* @return cold spawn task; starts on the scheduler when awaited
* @tparam Pyld payload type; the context is injected automatically when it is a context_payload
* @param task coroutine to run on the context
* @param args extra arguments to construct the payload with (besides the auto-injected context)
* @return cold spawn task; starts on the context when awaited
*/
template<payload Pyld = this_coro::default_payload, awaitable Awaitable, class... Args>
[[nodiscard]] auto spawn(Awaitable task, Args&&... args) const -> spawn_task<awaited_t<Awaitable>> {
if constexpr(this_coro::scheduler_payload<Pyld>)
return spawn_impl(Pyld{scheduler(), std::forward<Args>(args)...}, *this, std::move(task));
if constexpr(this_coro::context_payload<Pyld>)
return spawn_impl(Pyld{context(), std::forward<Args>(args)...}, *this, std::move(task));
else
return spawn_impl(Pyld{std::forward<Args>(args)...}, *this, std::move(task));
}
Expand All @@ -87,20 +87,20 @@ class executor {
-> spawn_task<awaited_t<Awaitable>> {
co_await self.schedule();

// the final awaiter of the spawn_task resumes the caller on its own scheduler
// the final awaiter of the spawn_task resumes the caller on its own context
if constexpr(this_coro_awaitable<Awaitable>)
co_return co_await co::steal(std::move(task), payload); // injects the payload, no hop
else
co_return co_await task;
}

co::scheduler const* _sched;
co::context const* _context;

public:
/**
* gets underlying scheduler
* gets underlying context
*/
constexpr co::scheduler const& scheduler() const { return *_sched; }
constexpr co::context const& context() const { return *_context; }

constexpr bool operator==(executor const&) const = default;

Expand All @@ -117,7 +117,7 @@ class executor {
namespace cth::co::this_coro {
struct [[nodiscard]] executor_tag : tag_base {
[[nodiscard]] static constexpr auto operator()(default_payload const& p) {
return data_awaiter{executor{p.scheduler()}};
return data_awaiter{executor{p.context()}};
}
};

Expand Down
Loading
Loading