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>
using namespace boost::capy;
// A minimal single-threaded execution context.
// Demonstrates how to satisfy the Executor concept
// for any custom scheduling system.
class run_loop : public 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;
execution_context& context() const noexcept
{
return *loop_;
}
void on_work_started() const noexcept {}
void on_work_finished() const noexcept {}
std::coroutine_handle<> dispatch(
std::coroutine_handle<> h) const
{
if (loop_->is_running_on_this_thread())
return h;
loop_->enqueue(h);
return std::noop_coroutine();
}
void post(std::coroutine_handle<> h) const
{
loop_->enqueue(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(Executor<run_loop::executor_type>);
task<int> compute(int x)
{
std::cout << " computing " << x << " * " << x << "\n";
co_return x * x;
}
task<> run_tasks()
{
std::cout << "Launching 3 tasks with when_all...\n";
auto [a, b, c] = co_await 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
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 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(h)— resume immediately if already on this context, otherwise enqueue -
post(h)— always enqueue for later execution -
operator==— compare two executors for identity
static_assert(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(
std::coroutine_handle<> h) const
{
if (loop_->is_running_on_this_thread())
return h; // resume inline
loop_->enqueue(h);
return std::noop_coroutine(); // defer
}
dispatch checks whether the caller is already running on the loop’s thread. If so, it returns the handle directly for inline resumption. Otherwise it enqueues 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.