Cancellation

This page explains how to cancel running coroutines using std::stop_token.

Code snippets assume using namespace boost::capy; is in effect.

Cooperative Cancellation

Capy supports cooperative cancellation through std::stop_token. When a task is launched with stop support, the token propagates through the entire call chain automatically.

Cooperative means:

  • The framework delivers cancellation requests to operations

  • Operations check the token and decide how to respond

  • Nothing is forcibly terminated

How Stop Tokens Propagate

Stop tokens propagate through co_await chains just like affinity. When you await a stoppable operation inside a task with a stop token, the token is forwarded automatically:

task<void> cancellable_work()
{
    // If this task has a stop token, it's automatically
    // passed to any stoppable awaitables we co_await
    co_await some_stoppable_operation();
}

The Stoppable Awaitable Protocol

Awaitables that support cancellation implement the stoppable_awaitable concept. Their await_suspend receives both a dispatcher and a stop token:

template<typename Dispatcher>
auto await_suspend(
    std::coroutine_handle<> h,
    Dispatcher const& d,
    std::stop_token token)
{
    if (token.stop_requested())
    {
        // Already cancelled, resume immediately
        return d(h);
    }

    // Start async operation with cancellation support
    start_async([h, &d, token] {
        if (token.stop_requested())
        {
            // Handle cancellation
        }
        d(h);
    });
    return std::noop_coroutine();
}

Implementing a Stoppable Timer

Here is a complete example of a stoppable timer:

struct stoppable_timer
{
    std::chrono::milliseconds duration_;
    bool cancelled_ = false;

    bool await_ready() const noexcept
    {
        return duration_.count() <= 0;
    }

    // Affine path (no cancellation)
    template<typename Dispatcher>
    auto await_suspend(any_coro h, Dispatcher const& d)
    {
        start_timer(duration_, [h, &d] { d(h); });
        return std::noop_coroutine();
    }

    // Stoppable path (with cancellation)
    template<typename Dispatcher>
    auto await_suspend(
        any_coro h,
        Dispatcher const& d,
        std::stop_token token)
    {
        if (token.stop_requested())
        {
            cancelled_ = true;
            return d(h);  // Resume immediately
        }

        auto timer_handle = start_timer(duration_, [h, &d] { d(h); });

        // Cancel timer if stop requested
        std::stop_callback cb(token, [timer_handle] {
            cancel_timer(timer_handle);
        });

        return std::noop_coroutine();
    }

    void await_resume()
    {
        if (cancelled_)
            throw std::runtime_error("operation cancelled");
    }
};

Key points:

  • Provide both await_suspend overloads (with and without token)

  • Check stop_requested() before starting work

  • Register a stop_callback to cancel the underlying operation

  • Signal cancellation in await_resume (typically via exception)

Checking Cancellation Status

Within a coroutine, use co_await get_stop_token() to retrieve the current stop token and check if cancellation was requested:

task<void> long_running_work()
{
    auto token = co_await get_stop_token();

    for (int i = 0; i < 1000; ++i)
    {
        if (token.stop_requested())
            co_return;  // Exit gracefully

        co_await process_chunk(i);
    }
}

The get_stop_token() function returns a tag that the promise intercepts via await_transform. This operation never suspends—await_ready() always returns true.

If no stop token was propagated to this coroutine, the returned token has stop_possible() == false.

Supporting get_stop_token in Custom Coroutines

If you define your own coroutine type and want to support get_stop_token(), inherit your promise type from stop_token_support:

struct my_task
{
    struct promise_type : stop_token_support<promise_type>
    {
        my_task get_return_object();
        std::suspend_always initial_suspend() noexcept;
        std::suspend_always final_suspend() noexcept;
        void return_void();
        void unhandled_exception();
    };

    std::coroutine_handle<promise_type> h_;

    // ... awaitable interface ...
};

The mixin provides:

  • Storage for the stop token (private)

  • set_stop_token(token) — call from your stoppable await_suspend

  • stop_token() — getter for internal use

  • await_transform — intercepts get_stop_token() calls

If you need custom awaitable transformation, override transform_awaitable instead of await_transform:

struct promise_type : stop_token_support<promise_type>
{
    any_dispatcher ex_;

    template<typename A>
    auto transform_awaitable(A&& a)
    {
        return make_affine(std::forward<A>(a), ex_);
    }
};

To make your coroutine type satisfy stoppable_awaitable (receive tokens when awaited), implement the stoppable await_suspend overload:

template<dispatcher D>
any_coro await_suspend(any_coro cont, D const& d, std::stop_token token)
{
    h_.promise().set_stop_token(token);
    // ... rest of suspend logic ...
}

When NOT to Use Cancellation

Use cancellation when:

  • Operations may take a long time

  • Users need to abort operations

  • Timeouts are required

Do NOT use cancellation when:

  • Operations are very short — the overhead is not worth it

  • Operations cannot be interrupted meaningfully

  • You need guaranteed completion

Summary

Concept Description

Cooperative

Operations check the token and decide how to respond

Automatic propagation

Tokens flow through co_await chains

get_stop_token()

Retrieve the current stop token inside a coroutine

stop_token_support

CRTP mixin for custom coroutines to support get_stop_token()

stoppable_awaitable

Concept for awaitables that support cancellation

stop_callback

Register cleanup when cancellation is requested

Next Steps