Repository navigation
Unwind abandoned coroutine chains from their owner - #453
Conversation
|
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 |
|
GCOVR code coverage report https://453.capy.prtest3.cppalliance.org/gcovr/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.
10518c1 to
0515aa3
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 38 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
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.
0515aa3 to
67a4a0d
Compare
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
chain_ownerin the frame that started it:run_async,runonto another executor, or awhen_all/when_anybranch.io_env::ownercarries the owner down the segment. An awaitable copies it into thecontinuationit hands to a holder.capy::abandon. The owner is then posted once to its executor.taskcallsstd::terminateif an awaited frame is destroyed outside an unwind.Nothing allocates.
Contract changes
continuationandio_envgainowner, which defaults to null.Executor: after shutdown,postanddispatchabandon the continuation. Apostthat throws leaves the continuation with the caller.IoAwaitable: an awaitable that keeps its coroutine storescontinuation{h, nullptr, env->owner}, and abandons it on teardown.Behaviour changes
async_mutex,async_eventorasync_wakerresumes its waiters withcanceled. A waiter whose executor has shut down is abandoned instead.Also fixed
write_nowteardown, 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.