Custom Executor
Implementing the Executor concept with a single-threaded run loop.
What This Example Shows
-
Satisfying the
Executorconcept -
Implementing
execution_context,dispatch, andpost -
Running Capy coroutines on a custom scheduling system
Source Code
#include <boost/capy.hpp>
#include <boost/capy/ex/frame_allocator.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<capy::continuation> queue_;
std::thread::id owner_;
public:
class executor_type;
run_loop()
: execution_context(this)
{
}
~run_loop()
{
// Once torn down, the loop runs nothing, not even inline.
owner_ = {};
// Nothing will run these now: give them up, so their chains
// are unwound. An unwind posted here is given up in turn.
auto drain = [this]
{
while (!queue_.empty())
{
auto c = queue_.front();
queue_.pop();
capy::abandon(c);
}
};
shutdown();
drain();
destroy();
// Services give up their work in shutdown(), but anything a
// destructor posts anyway is still given up.
drain();
}
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 c = queue_.front();
queue_.pop();
capy::safe_resume(c.h);
}
}
// A copy keeps the owner, which abandon needs.
void enqueue(capy::continuation const& c)
{
queue_.push(c);
}
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; // resume inline
loop_->enqueue(c);
return std::noop_coroutine(); // defer
}
void post(capy::continuation& c) const
{
loop_->enqueue(c);
}
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<capy::results<int>> compute(int x)
{
std::cout << " computing " << x << " * " << x << "\n";
co_return capy::results<int>{std::error_code(), x * x};
}
capy::task<> run_tasks()
{
std::cout << "Starting 3 tasks with when_all...\n";
auto [ec, r1, r2, r3] = co_await capy::when_all(
compute(3), compute(7), compute(11));
std::cout << "\nResults: " << r1 << ", " << r2
<< ", " << r3 << "\n";
std::cout << "Sum of squares: "
<< r1 + r2 + r3 << "\n";
}
int main()
{
run_loop loop;
// Start 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)
{
}
};
The destructor must call shutdown(), give up any work still queued with abandon, then call destroy(). The drain also takes work posted while it runs. That includes the work of a service an unwound chain creates, which is shut down as it is created. Services give up their work in shutdown(), so destroying them posts nothing. The destructor drains once more for stray work a destructor posts anyway. No service may be created after destroy(). The queue holds copies of each continuation, so the owner that abandon needs travels with the handle.
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>);
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);
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 a copy of c and returns noop_coroutine so the caller continues without blocking.
post always enqueues, even if already on the right thread.
Driving the Loop
capy::run_async(loop.get_executor())(run_tasks());
loop.run();
run_async enqueues the initial coroutine. loop.run() drains the queue, resuming coroutines one by one until all work completes.
run() uses capy::safe_resume(h) instead of h.resume(). This saves and restores the thread-local frame allocator around each resumption, preventing coroutines from spoiling each other’s allocator. All custom executor event loops must use safe_resume — see TLS Preservation for details.
Output
Running event loop on main thread...
Starting 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.
Exercises
-
Add a
stop()method that causesrun()to exit early, even with work remaining -
Make the run loop thread-safe so work can be posted from other threads
-
Integrate the run loop with a platform event system (e.g.,
epollorkqueue). For the GUI-framework case, GUI Integration works the example through, including where a coroutine resumes after awaiting work that finished on another thread