diff --git a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc b/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc index 2e24c66f..9e8ee15e 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc @@ -216,7 +216,7 @@ 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(); @@ -224,6 +224,17 @@ task<> cancellable_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 diff --git a/include/boost/capy/ex/io_awaitable_promise_base.hpp b/include/boost/capy/ex/io_awaitable_promise_base.hpp index 6360f3b2..6a918537 100644 --- a/include/boost/capy/ex/io_awaitable_promise_base.hpp +++ b/include/boost/capy/ex/io_awaitable_promise_base.hpp @@ -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(); } diff --git a/include/boost/capy/quitter.hpp b/include/boost/capy/quitter.hpp index 0ef64cc5..c4be6fef 100644 --- a/include/boost/capy/quitter.hpp +++ b/include/boost/capy/quitter.hpp @@ -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 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; diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index 979bd33f..ce2a5b55 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.hpp @@ -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 handle() const noexcept { return h_; @@ -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.