Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,25 @@ Inside a task, use `get_stop_token()` to access the current stop token:
task<> cancellable_work()
{
auto token = co_await get_stop_token();

while (!token.stop_requested())
{
co_await do_chunk_of_work();
}
}
----

=== Why Not `coroutine_handle::destroy()`?

`std::coroutine_handle::destroy()` is the {cpp}20 primitive that frees a coroutine frame. It is not a cancellation mechanism, and it has the same flaw as forceful thread interruption: the coroutine is torn down with no opportunity to complete pending I/O, release locks, or run RAII destructors in the expected order.

Capy exposes `task::handle()` and `quitter::handle()` so that Capy's own launchers (`run_async`, `run`) and custom integrations can dispatch coroutines through executors. Calling `destroy()` on such a handle while the coroutine is being awaited by a parent produces undefined behavior: the destruction cascades back through the parent's continuation, re-entering frame destruction that is already in progress.

The rule:

* To cancel work, request a stop on a `std::stop_source` whose token the work observes. The work unwinds cleanly through `final_suspend` and any RAII guards run in the correct order.
* Do not call `destroy()` on a handle returned by `task::handle()` or `quitter::handle()` while the coroutine is being awaited.

== Part 6: Responding to Cancellation

=== Checking the Token
Expand Down
8 changes: 7 additions & 1 deletion include/boost/capy/ex/io_awaitable_promise_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,13 @@ class io_awaitable_promise_base
public:
~io_awaitable_promise_base()
{
// Abnormal teardown: destroy orphaned continuation
// Abnormal teardown: destroy an orphaned continuation, e.g.
// a run_async trampoline when the task is destroyed before
// reaching final_suspend. Callers must not destroy a task
// via handle().destroy() while it is being awaited by a
// parent coroutine: that puts cont_ under another owner
// and would produce a double-destroy from this branch. See
// task::handle() / quitter::handle() for the contract.
if(cont_ != std::noop_coroutine())
cont_.destroy();
}
Expand Down
20 changes: 18 additions & 2 deletions include/boost/capy/quitter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,29 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
return h_;
}

/// Return the coroutine handle.
/** Return the coroutine handle.

@note Do not call `destroy()` on the returned handle while
the quitter is being awaited. The quitter's lifetime is
normally managed by `run_async`, `run`, or the awaiting
parent; manually destroying a suspended quitter that another
coroutine is awaiting produces undefined behavior. For
cooperative cancellation, use `std::stop_token`.

@return The coroutine handle.
*/
std::coroutine_handle<promise_type> handle() const noexcept
{
return h_;
}

/// Release ownership of the coroutine frame.
/** Release ownership of the coroutine frame.

@note If the caller intends to call `destroy()` on the
released handle, it must do so only when the quitter has not
started or has fully completed. Destroying a suspended
quitter that is being awaited produces undefined behavior.
*/
void release() noexcept
{
h_ = nullptr;
Expand Down
17 changes: 16 additions & 1 deletion include/boost/capy/task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,17 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
return h_;
}

/// Return the coroutine handle.
/** Return the coroutine handle.

@note Do not call `destroy()` on the returned handle while the
task is being awaited. The task's lifetime is normally managed
by `run_async`, `run`, or the awaiting parent; manually
destroying a suspended task that another coroutine is awaiting
produces undefined behavior. For cooperative cancellation, use
`std::stop_token`.

@return The coroutine handle.
*/
std::coroutine_handle<promise_type> handle() const noexcept
{
return h_;
Expand All @@ -275,6 +285,11 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
coroutine frame. The caller becomes responsible for the frame's
lifetime.

@note If the caller intends to call `destroy()` on the
released handle, it must do so only when the task has not
started or has fully completed. Destroying a suspended task
that is being awaited produces undefined behavior.

@par Postconditions
`handle()` returns the original handle, but the task no longer
owns it.
Expand Down
Loading