Custom Executor
Implementing the Executor concept with a single-threaded run loop.
What You Will Learn
-
Satisfying the
Executorconcept -
Implementing
execution_context,dispatch, andpost -
Running Capy coroutines on a custom scheduling system
Prerequisites
-
Understanding of executors from Executors and Execution Contexts
Source Code
#include <boost/capy.hpp>
#include <iostream>
#include <queue>
#include <thread>
namespace capy = boost::capy;
// A minimal single-threaded execution context.
// Demonstrates how to satisfy the Executor concept
// for any custom scheduling system.
class run_loop : public capy::execution_context
{
std::queue<std::coroutine_handle<>> queue_;
std::thread::id owner_;
public:
class executor_type;
run_loop()
: execution_context(this)
{
}
~run_loop()
{
shutdown();
destroy();
}
run_loop(run_loop const&) = delete;
run_loop& operator=(run_loop const&) = delete;
// Drain the queue until empty
void run()
{
owner_ = std::this_thread::get_id();
while (!queue_.empty())
{
auto h = queue_.front();
queue_.pop();
h.resume();
}
}
void enqueue(std::coroutine_handle<> h)
{
queue_.push(h);
}
bool is_running_on_this_thread() const noexcept
{
return std::this_thread::get_id() == owner_;
}
executor_type get_executor() noexcept;
};
class run_loop::executor_type
{
friend class run_loop;
run_loop* loop_ = nullptr;
explicit executor_type(run_loop& loop) noexcept
: loop_(&loop)
{
}
public:
executor_type() = default;
capy::execution_context& context() const noexcept
{
return *loop_;
}
void on_work_started() const noexcept {}
void on_work_finished() const noexcept {}
std::coroutine_handle<> dispatch(
capy::continuation& c) const
{
if (loop_->is_running_on_this_thread())
return c.h;
loop_->enqueue(c.h);
return std::noop_coroutine();
}
void post(capy::continuation& c) const
{
loop_->enqueue(c.h);
}
bool operator==(executor_type const& other) const noexcept
{
return loop_ == other.loop_;
}
};
inline
run_loop::executor_type
run_loop::get_executor() noexcept
{
return executor_type{*this};
}
// Verify the concept is satisfied
static_assert(capy::Executor<run_loop::executor_type>);
capy::task<int> compute(int x)
{
std::cout << " computing " << x << " * " << x << "\n";
co_return x * x;
}
capy::task<> run_tasks()
{
std::cout << "Launching 3 tasks with when_all...\n";
auto [a, b, c] = co_await capy::when_all(
compute(3),
compute(7),
compute(11));
std::cout << "\nResults: " << a << ", " << b << ", " << c
<< "\n";
std::cout << "Sum of squares: " << a + b + c << "\n";
}
int main()
{
run_loop loop;
// Launch using run_async, just like with thread_pool
capy::run_async(loop.get_executor())(run_tasks());
// Drive the loop — all coroutines execute here
std::cout << "Running event loop on main thread...\n";
loop.run();
std::cout << "Event loop finished.\n";
return 0;
}
Build
add_executable(custom_executor custom_executor.cpp)
target_link_libraries(custom_executor PRIVATE Boost::capy)
Walkthrough
Inheriting execution_context
class run_loop : public capy::execution_context
{
// ...
run_loop()
: execution_context(this)
{
}
};
Custom execution contexts inherit from execution_context and pass this to the base constructor. The destructor must call shutdown() then destroy() to clean up coroutine state.
The Executor Concept
The nested executor_type must provide:
-
context()— returns a reference to the owningexecution_context -
on_work_started()/on_work_finished()— work-tracking hooks -
dispatch(c)— resume immediately if already on this context, otherwise enqueue. Takes acontinuation&and returnsstd::coroutine_handle<>. -
post(c)— always enqueue for later execution. Takes acontinuation&. -
operator==— compare two executors for identity
static_assert(capy::Executor<run_loop::executor_type>);
The static_assert verifies at compile time that all concept requirements are met.
Dispatch vs Post
std::coroutine_handle<> dispatch(
capy::continuation& c) const
{
if (loop_->is_running_on_this_thread())
return c.h; // resume inline
loop_->enqueue(c.h);
return std::noop_coroutine(); // defer
}
dispatch takes a continuation& and checks whether the caller is already running on the loop’s thread. If so, it returns c.h directly for inline resumption via symmetric transfer. Otherwise it enqueues c.h and returns noop_coroutine so the caller continues without blocking.
post always enqueues, even if already on the right thread.
Output
Running event loop on main thread...
Launching 3 tasks with when_all...
computing 3 * 3
computing 7 * 7
computing 11 * 11
Results: 9, 49, 121
Sum of squares: 179
Event loop finished.