Skip to content

Unwind abandoned coroutine chains from their owner - #453

Merged
sgerbino merged 5 commits into
cppalliance:developfrom
sgerbino:pr/lifetime-executor
Oct 16, 2026
Merged

sgerbino merged 5 commits into
cppalliance:developfrom
sgerbino:pr/lifetime-executor

Conversation

@sgerbino

Copy link
Copy Markdown
Collaborator

Problem

A coroutine suspended in something torn down, such as a strand, thread pool or context, stays suspended forever. Its frames leak. A holder has only the innermost coroutine's handle. Destroying that handle leaves the parent frame holding a destroyed frame.

Design

  • Each segment of a chain has an owner, a chain_owner in the frame that started it: run_async, run onto another executor, or a when_all/when_any branch.
  • io_env::owner carries the owner down the segment. An awaitable copies it into the continuation it hands to a holder.
  • A holder that will never resume a continuation calls capy::abandon. The owner is then posted once to its executor.
  • The owner destroys its segment from the top frame down. Destructors run; no code after a suspension point runs, and no handler is called.
  • The unwind then moves to the segment above.
  • A shut-down executor abandons the owner's continuation instead of queuing it, and the owner's destructor runs the same unwind.
  • task calls std::terminate if an awaited frame is destroyed outside an unwind.

Nothing allocates.

Contract changes

  • continuation and io_env gain owner, which defaults to null.
  • Executor: after shutdown, post and dispatch abandon the continuation. A post that throws leaves the continuation with the caller.
  • IoAwaitable: an awaitable that keeps its coroutine stores continuation{h, nullptr, env->owner}, and abandons it on teardown.

Behaviour changes

  • Strands, thread pools and contexts abandon queued work at teardown.
  • Destroying an async_mutex, async_event or async_waker resumes its waiters with canceled. A waiter whose executor has shut down is abandoned instead.

Also fixed

  • The frame allocator across threads and shared libraries.
  • Type-erased streams destroyed with an operation in flight.
  • write_now teardown, and its transfer under MSVC.

Documentation

A new Lifetime and Ownership page, and teardown notes in the existing guides.

Testing

Teardown tests run under ASan and LSan. Every commit builds and passes on its own, and the full CI matrix is green.

Resolves #406.

@cppalliance-bot

cppalliance-bot commented Oct 15, 2026 •

Copy link
Copy Markdown

An automated preview of the documentation is available at https://453.capy.prtest3.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-10-16 14:49:09 UTC

@cppalliance-bot

cppalliance-bot commented Oct 15, 2026 •

Copy link
Copy Markdown

GCOVR code coverage report https://453.capy.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://453.capy.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff Report https://453.capy.prtest3.cppalliance.org/diff-report/index.html

Build time: 2026-10-16 15:03:26 UTC

…ries

A coroutine resumed on another thread read the frame allocator slot
of the thread it suspended on under MSVC. The allocator slot and the
thread's frame cache were each defined once per shared library, so a
frame allocated in one library could be cached or freed against
another's state. A thread also kept caching frames after its cache
had been drained at exit.

The slot is read on the resuming thread, both the slot and the cache
live in the library, and a drained cache sends frees straight to the
upstream resource.
@sgerbino
sgerbino force-pushed the pr/lifetime-executor branch 2 times, most recently from 10518c1 to 0515aa3 Compare October 16, 2026 13:57
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.70458% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.27%. Comparing base (f764adc) to head (67a4a0d).
⚠️ Report is 6 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/capy/ex/async_waker.hpp 95.23% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #453      +/-   ##
===========================================
+ Coverage    98.09%   98.27%   +0.18%     
===========================================
  Files          130      137       +7     
  Lines         6289     7316    +1027     
===========================================
+ Hits          6169     7190    +1021     
- Misses         120      126       +6     
Flag Coverage Δ
linux 98.15% <ø> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
include/boost/capy/detail/abandonable.hpp 100.00% <100.00%> (ø)
include/boost/capy/detail/intrusive.hpp 100.00% <ø> (ø)
include/boost/capy/detail/owned_range.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/abandon.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/async_event.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/async_mutex.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/execution_context.hpp 100.00% <ø> (ø)
include/boost/capy/ex/frame_alloc_mixin.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/frame_allocator.hpp 100.00% <ø> (ø)
...nclude/boost/capy/ex/io_awaitable_promise_base.hpp 100.00% <ø> (ø)
... and 23 more

... and 38 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f764adc...67a4a0d. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

A coroutine suspended in something that is torn down, such as an
executor, strand, thread pool or blocking context being destroyed,
was left suspended forever, and its frames and everything they held
leaked. Destroying the coroutine alone was no better: the frame
awaiting it would then hold a destroyed child.

Every segment of a chain now has an owner: the coroutine that started
it with run_async, run on another executor, or a when_all or when_any
branch. Awaitables carry the owner in the continuation they hand to a
holder. A holder that will never resume a continuation passes it to
abandon, which posts the owner to its own executor; there the owner
destroys the segment from its top frame down and hands the unwind to
the segment above it. A frame still awaited is destroyed only during
such an unwind; anything else terminates.

Executors abandon what is posted to them after shutdown, and a post
that throws leaves the continuation with the caller. Strands, thread
pools, blocking contexts and execution contexts drain and abandon
their queued work at teardown. Async mutexes, events and wakers cancel
their waiters when destroyed. A join keeps its stop state alive while
its stop request runs. The examples carry the owner through their
awaitables. A test stream carries the owner of the read it parks, completes
at once a read whose stop was requested before it parks, and leaves a
woken read queued as posted.
A type-erased stream destroyed or reassigned while its operation was
parked destroyed the stream before the operation that referenced it,
and an awaitable unwound after its stream was gone read the stream.
The stream now ends its operation first, and an awaitable stops
owning the operation once the stream does.

write_now's caller is unwound with its parked write, and write_now
transfers to the next coroutine and restores the frame allocator as a
task does.
The executor, strand and thread pool pages, the cancellation guide and
the examples now say what happens to a suspended coroutine when what
holds it is torn down, and show awaitables carrying their chain's
owner.
The rules for composing coroutines safely were spread across the pages
on lambda captures, executors, cancellation, composition and buffers.
One page now gathers them: what a frame keeps alive, which arguments
are safe to pass by reference, who owns a suspended coroutine, what
an abandoned chain runs as it is unwound, and the order to stop work
and destroy what it runs on. Its fragments are compiled and run with
the documentation tests.
@sgerbino
sgerbino force-pushed the pr/lifetime-executor branch from 0515aa3 to 67a4a0d Compare October 16, 2026 14:45
@sgerbino
sgerbino merged commit 43ff48b into cppalliance:develop Oct 16, 2026
41 checks passed
@sgerbino
sgerbino deleted the pr/lifetime-executor branch October 16, 2026 15:19
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Beast2 Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

docs: add a lifetime and ownership page for composing coroutines

2 participants