From 1deee9988b93269814196e0f5a2e8f8852e9a051 Mon Sep 17 00:00:00 2001 From: Mateusz Jakub Fila Date: Thu, 12 Mar 2026 21:49:05 +0100 Subject: [PATCH 1/2] add schedule on algorithm compatible with alien coroutine --- examples/CMakeLists.txt | 3 +- examples/alien_schedule_on.cpp | 74 ++++++++ include/CoroutineTests/alien/schedule_on.hpp | 179 +++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 examples/alien_schedule_on.cpp create mode 100644 include/CoroutineTests/alien/schedule_on.hpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c66df80..c438c5a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -34,7 +34,8 @@ add_executable(alien_sync_wait alien_sync_wait.cpp) target_link_libraries(alien_sync_wait PRIVATE CoroutineTests) add_executable(alien_counting_scope alien_counting_scope.cpp) target_link_libraries(alien_counting_scope PRIVATE CoroutineTests) - +add_executable(alien_schedule_on alien_schedule_on.cpp) +target_link_libraries(alien_schedule_on PRIVATE CoroutineTests) if(BUILD_STDEXEC) add_executable(exec_task_stdexec exec_task.cpp) diff --git a/examples/alien_schedule_on.cpp b/examples/alien_schedule_on.cpp new file mode 100644 index 0000000..177372b --- /dev/null +++ b/examples/alien_schedule_on.cpp @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "CoroutineTests/alien/schedule_on.hpp" +#include "CoroutineTests/alien/sync_wait.hpp" +#include "CoroutineTests/alien/tool.hpp" +#include "CoroutineTests/threadpool.hpp" +#include "alien_timer.hpp" // AsyncTimer +#include "logging_utils.hpp" // log, format_name + +using namespace CoroutineTests::alien; + +tool::Task inner_most(std::string_view parent) { + const auto self = format_name(parent, "inner_most"); + log(self) << "Starting inner_most" << std::endl; + log(self) << "Calling async API in inner_most" << std::endl; + auto status = co_await AsyncTimer{std::chrono::milliseconds(75), + StatusCode::SUCCESS, self}; + log(self) << "Result from async API in inner_most: " << status << std::endl; + log(self) << "Finishing inner_most" << std::endl; + co_return; +} + +tool::Task inner(std::string_view parent) { + const auto self = format_name(parent, "inner"); + log(self) << "Starting inner" << std::endl; + co_await inner_most(self); + log(self) << "Finishing inner" << std::endl; + co_return tool::StatusCode::SUCCESS; +} + +tool::Task outer( + std::function)> inner_scheduler, + std::string_view parent) { + const auto self = format_name(parent, "outer"); + log(self) << "Starting outer" << std::endl; + auto status = co_await schedule_on(inner_scheduler, inner(self)); + log(self) << "Received status " << status << " from inner" << std::endl; + log(self) << "Finishing outer" << std::endl; + co_return; +} + +tool::Task outer_most( + std::function)> inner_scheduler, + std::string_view parent) { + const auto self = format_name(parent, "outer_most"); + log(self) << "Starting outer_most" << std::endl; + co_await outer(inner_scheduler, self); + log(self) << "Finishing outer_most" << std::endl; + co_return tool::StatusCode::SUCCESS; +} + +int main() { + log() << "main Starting" << std::endl; + CoroutineTests::Threadpool threadpool(1); + auto scheduler = [&threadpool](std::coroutine_handle<> handle) { + log() << "scheduler called, enqueuing work" << std::endl; + threadpool.enqueue_task(handle); + }; + + CoroutineTests::Threadpool inner_threadpool(1); + auto inner_scheduler = [&inner_threadpool](std::coroutine_handle<> handle) { + log() << "inner_scheduler called, enqueuing work" << std::endl; + inner_threadpool.enqueue_task(handle); + }; + + log() << "main Launching outer_most and waiting for completion..." + << std::endl; + auto status = CoroutineTests::alien::sync_wait( + scheduler, outer_most(inner_scheduler, "main")); + log() << "main Final status of outer_most " << status << "" << std::endl; + return 0; +} diff --git a/include/CoroutineTests/alien/schedule_on.hpp b/include/CoroutineTests/alien/schedule_on.hpp new file mode 100644 index 0000000..b4f4b41 --- /dev/null +++ b/include/CoroutineTests/alien/schedule_on.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace CoroutineTests::alien { + +namespace detail::schedule_on { +namespace concepts { +template +concept HasScheduler = requires(T t) { + { + t.get_scheduler() + } -> std::convertible_to)>>; +}; +} // namespace concepts + +// Helper coroutine type allowing for having different scheduler than parent +// coroutine. +template +class [[nodiscard]] Task { + + static_assert(std::movable || std::same_as, + "Task requires ResultType to be movable or void"); + + public: + using result_type = ResultType; + + struct promise_type; // typedef required by coroutines + using handle_type = + std::coroutine_handle; // not required but useful + + // Constructor from coroutine handle + Task(handle_type coroutine_handle) : m_coroutine(coroutine_handle) {} + ~Task() { + if (m_coroutine) { + m_coroutine.destroy(); + } + } + Task() = default; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + Task(Task&& other) noexcept : m_coroutine{other.m_coroutine} { + other.m_coroutine = {}; + } + Task& operator=(Task&& other) noexcept { + if (this != &other) { + if (m_coroutine) { + m_coroutine.destroy(); + } + m_coroutine = other.m_coroutine; + other.m_coroutine = {}; + } + return *this; + } + + // Awaitable interface: always suspend to allow async execution + bool await_ready() const noexcept { return false; } + // Awaitable interface: setup parent relationship, suspend parent and + // schedule this coroutine on its scheduler + template + inline void await_suspend(std::coroutine_handle handle) noexcept; + // Awaitable interface: return result or rethrow exception on resume + result_type await_resume() const; + + private: + handle_type m_coroutine = nullptr; +}; + +// Helper for handling co_return in promise_type, default implementation for +// non-void ResultType +template +struct ReturnHelper { + // Storage for the co_return result value + std::optional m_value; + + // Required by coroutines, mutually exclusive with return_void + // Store the co_return value + template + requires std::constructible_from + void return_value(T&& value) { + m_value.emplace(std::forward(value)); + } + // Overload to resolve ambiguity + void return_value(ResultType value) { m_value.emplace(std::move(value)); } +}; + +// Specialization for void return type +template <> +struct ReturnHelper { + // Required by coroutines, mutually exclusive with return_value + // Handle co_return without value + void return_void() {} +}; + +template +struct Task::promise_type + : public ReturnHelper::result_type> { + // Storage for exceptions thrown in the coroutine body + std::exception_ptr m_exception; + // Handle to the parent coroutine that co_awaited this task + std::coroutine_handle<> m_parent; + // Handle to scheduler to resume this coroutine and propagate to children + std::function)> m_scheduler; + // Handle to the scheduler to resume parent coroutine + std::function)> m_parent_scheduler; + + // Non-default constructor to pass the scheduler. The constructor will be + // used if coroutine function has the same signature. The unused parameters + // are here only to match the signature. + template + promise_type(std::function)> scheduler, + Coro&&) noexcept + : m_scheduler(scheduler) {} + + // Accessor for scheduler used by child coroutines + const auto& get_scheduler() const { return m_scheduler; } + // Schedule resumption of this task + void reschedule() { m_scheduler(handle_type::from_promise(*this)); } + + // Required by coroutines: create the object + Task get_return_object() { return {handle_type::from_promise(*this)}; } + // Required by coroutines: suspend immediately on start (lazy execution) + std::suspend_always initial_suspend() const { return {}; } + // Required by coroutines: handle completion and resume parent + auto final_suspend() const noexcept { + struct final_awaiter { + // Don't skip final suspension + bool await_ready() const noexcept { return false; } + // Resume parent coroutine on its own scheduler + void await_suspend(handle_type handle) noexcept { + auto parent = handle.promise().m_parent; + auto parent_scheduler = handle.promise().m_parent_scheduler; + if (parent && parent_scheduler) { + parent_scheduler(parent); + } + } + // No action needed on resume + void await_resume() const noexcept {} + }; + return final_awaiter{}; + } + // Required by coroutines: capture exceptions for later rethrowing + void unhandled_exception() { m_exception = std::current_exception(); } +}; + +template +template +inline void Task::await_suspend( + std::coroutine_handle handle) noexcept { + m_coroutine.promise().m_parent = handle; + m_coroutine.promise().m_parent_scheduler = handle.promise().get_scheduler(); + m_coroutine.promise().reschedule(); +} + +template +inline typename Task::result_type Task::await_resume() + const { + if (m_coroutine.promise().m_exception) { + std::rethrow_exception(m_coroutine.promise().m_exception); + } + if constexpr (std::same_as) { + return; + } else { + return std::move(m_coroutine.promise().m_value).value(); + } +} +} // namespace detail::schedule_on + +template +auto schedule_on(std::function)>, Coro coro) + -> detail::schedule_on::Task { + co_return co_await coro; +} + +} // namespace CoroutineTests::alien From ccbee5da7543e332ff17286086dccaf293fdb10b Mon Sep 17 00:00:00 2001 From: Mateusz Jakub Fila Date: Thu, 12 Mar 2026 21:49:24 +0100 Subject: [PATCH 2/2] update examples readme --- examples/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/README.md b/examples/README.md index 1d9ce3e..bda51f3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -144,6 +144,12 @@ Link: [alien_counting_scope.cpp](alien_counting_scope.cpp) This example demonstrates dynamic work submitting with `counting_scope` compatible with coroutine semantic as in ["Alien" example](#alien). `spawn` schedules execution of a coroutine that returns `void`. `join()` blocks the current thread until all submitted coroutines are finished (and rethrows the first exception captured from submitted work, if any). +## Alien schedule_on + +Link: [alien_schedule_on.cpp](alien_schedule_on.cpp) + +This example demonstrates changing scheduler used by part of a chain of coroutines following the semantics from ["Alien" example](#alien). The `schedule_on` algorithm can be used to adapt a coroutine to use a different scheduler than its parent. Starting the coroutine suspends its parent and reschedule work on the new scheduler, finishing the coroutine reschedules continuation of its parent using parent's scheduler. + ## Capy task Link: [capy_task.cpp](capy_task.cpp)