Custom Executor

Implementing the Executor concept with a single-threaded run loop.

What This Example Shows

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]
        {
            bool any = false;
            while (!queue_.empty())
            {
                auto c = queue_.front();
                queue_.pop();
                any = true;
                capy::abandon(c);
            }
            return any;
        };
        shutdown();
        drain();
        // A service's destructor may post too: repeat until nothing
        // is left of either.
        do
        {
            destroy();
        }
        while (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(). Destroying a service can post more work, and unwinding an abandoned chain can create a service, so the destructor repeats destroy() and the drain until a drain finds nothing. 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:

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

  1. Add a stop() method that causes run() to exit early, even with work remaining

  2. Make the run loop thread-safe so work can be posted from other threads

  3. Integrate the run loop with a platform event system (e.g., epoll or kqueue). For the GUI-framework case, GUI Integration works the example through, including where a coroutine resumes after awaiting work that finished on another thread