Repository navigation
fix(coro): captured_awaitable tag, steal payload by value, no frame leak, spawn_task returns to the caller - #22
Merged
Conversation
…eak on rvalue co_await, spawn_task returns to the caller - capture_awaiter_base is the public opt-in tag for captured awaitables, capture_promise_awaiter derives from it - steal takes its payload by value: a reference parameter of a coroutine dangles - task_base has no rvalue operator co_await: it extracted the handle into a non-owning awaiter and leaked the frame - scheduled_task -> spawn_task: a this_coro awaitable, the caller continues on its own scheduler - constexpr where it can run at compile time
This was referenced Oct 24, 2026
codethinki
added a commit
that referenced
this pull request
Oct 25, 2026
… and strand (#25) * Revert "fix(coro): captured_awaitable tag, steal payload by value, no frame leak on rvalue co_await, spawn_task returns to the caller (#22)" This reverts commit 48805f0. * fix(coro): task_base has no rvalue operator co_await - the rvalue overload extracted the handle into a non-owning awaiter, the frame leaked - an rvalue task binds to the const& overload, the temporary lives until the end of the full expression - the deleted copy assignment returns task_base& * fix(coro): every co_await returns the caller to its scheduler - the scheduler lives in the promise, the payload is user data only (payload_base) - task awaiters read the caller's promise in await_suspend, no inject step - affine final awaiter: a task continues its parent on the parent's scheduler, also after an exception - one marker for awaiters that resume the caller themselves (this_coro_awaiter_base), await_transform passes them through - steal is dev::steal, it captures a foreign awaitable, the other overloads and executor::steal are removed - scheduled_task -> spawn_task with its own awaiter, the spawned task gets a fresh payload - tags receive the scheduler and the payload - native_handle_awaiter_base and osfence_awaiter_base are not templates - sync_executor_task is removed - a this_coro task awaited by a caller without this_coro is a compile error * feat(coro): executor::sync_wait spawns a task and blocks until it ends - CTH_CRITICAL if called on a thread of its own scheduler, it would block a worker - the tests use it instead of their local helpers, the one in executor_test.cpp dropped exceptions of void tasks * feat(meta): nothrow_constructible and nothrow_copy_constructible concepts * refactor(coro): constexpr pass, copies are conditionally noexcept - constexpr where a function can run at compile time, removed where it reads the clock or returns noop_coroutine() - payload copies (promise constructor, inherit, await_suspend of the task awaiter), raw_promise::return_value and data_awaiter::await_resume are noexcept if the copy is: a throwing copy reaches the caller instead of terminating * docs: README coro section, every co_await returns the caller, sync_wait example * fix(win): keybd_event_awaiter derives the non-template osfence_awaiter_base * feat(coro): context interface, scheduler is a context - abstract context: post, await (handle, time point), owns_thread - co::current_context(): the context whose job runs on the calling thread, exact for strand jobs - promise, awaiters, tags and executor work on context const&, this_coro::scheduler -> this_coro::context - the affine final awaiter and schedule() compare with current_context() - sync_wait has no CTH_CRITICAL, it documents the self wait * feat(coro): strand, a serial queue on a scheduler - strand is a context, its jobs run one at a time and in post order on the workers of its scheduler - strand jobs set current_context() to the strand while they run - shared asio helpers post_on and await_on, scheduler and strand implement their overrides with them * refactor(coro): reuse promise and awaiter bases, data_awaiter moves its value - dev::affine_promise<T> holds the affine final awaiter and parentContext together, they cannot be split anymore - the task awaiters set their own parts and call basic_promise_awaiter::await_suspend - raw_promise_base holds exception, continuation and unhandled_exception for both raw_promise versions - data_awaiter moves its value out in await_resume * refactor(coro): one awaiter for all tasks, the promise attaches to the caller - this_coro_promise_awaiter is the awaiter of executor, spawn and capture tasks - the per task part is the promise's attach(): affine default sets the parent context, executor also inherits, spawn falls back to its own context - dev::nothrow_attachable gives the noexcept of await_suspend - capture_promise_awaiter, spawn_promise_awaiter and the unused basic_final_promise_awaiter are removed * refactor(coro): executor_promise::attach takes its noexcept from a trait pack, no lambda trick * perf(meta): trait packs are type level, type_pack - unpack_trait_t, first_pack_t and conditional_pack_t use partial specializations instead of consteval functions - about 2.6x faster on gcc and 15% on clang for 2000 instantiations - the consteval functions first_pack(), unpack_trait() and conditional_pack() are removed, the _t aliases stay - type_pack<T>: a pack that always unpacks to T * refactor(coro): caller_payload_t uses type_pack * fix(coro): start() on an active scheduler is a CTH_CRITICAL, callerContext, cleanups - start() on an active scheduler deadlocked: the jthread assignment joined a running worker - affine_promise::parentContext -> callerContext, caller is the term in the docs - unused includes, forward declaration and namespace removed, wait_awaiter uses chrono::time_point_t * fix(coro): a failed wait throws in the awaiting coroutine - context::await takes an await_func, the callback gets the error code of the wait - the wait handlers pass the error code instead of throwing on a worker, which terminated the program - timer_pool releases its timer before the callback, a failed wait leaked it - wait_awaiter_base keeps the error code until the caller resumes, its await_resume throws coro_exception - wait_awaiter -> time_point_awaiter, it and raw_native_handle_awaiter derive wait_awaiter_base - the awaiters of cth_net and cth_win call the base await_resume first * refactor(coro): CTH_CORO_WARN, CTH_CORO_STABLE_ERR, CTH_CORO_STABLE_THROW like the other modules * refactor(coro): context::current() instead of the free function current_context()
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Correctness fixes in cth_coro, found while designing the strand for jvk.
captured_awaitable was always false
capture_awaiter_base, which was only declared. The capture awaiters derived from a differently nameddev::capture_promise_awaiter_base, which nothing checked.capture_awaiter_baseis the public opt-in tag, andcapture_promise_awaiterderives from it.dev::capture_promise_awaiter_baseis gone.steal of a foreign awaitable crashed
Pyld const&. The callers pass a temporary, so the reference in the frame dangled before the task ran.Coroutine frames leaked on
co_awaitof an rvalue tasktask_base::operator co_await() &&extracted the handle into an awaiter that does not own it. LeakSanitizer found leaks in 38 of 66 coro tests, among them everyspawn.stealoverloads for this_coro awaitables return a non-owning awaiter: documented, co_await it in the same expression.spawn returns to the caller
scheduled_task->spawn_task. Before,spawn_implstole back to the target, and the caller only returned to its own scheduler because the broken concept sentscheduled_taskthrough the foreign path.spawn_taskis now a this_coro awaitable: the caller's scheduler is injected, and at the end the caller continues on its own scheduler (direct transfer if already there, else one post). A caller without a this_coro payload continues where the child ended.constexpr
Tests: classification
static_asserts, spawn behavior (A to B and back, A to A, void, exceptions, nested, a child that ends elsewhere, a caller without payload), steal of a foreign awaitable. 285/285 on gcc and clang, debug and release. ASan: 0 leaks across all coro tests.