Skip to content

fix(coro): correctness v2, every co_await returns the caller, context and strand - #25

Merged
codethinki merged 19 commits into
masterfrom
fix/coro-correctness-v2
Oct 25, 2026
Merged

codethinki merged 19 commits into
masterfrom
fix/coro-correctness-v2

Conversation

@codethinki

@codethinki codethinki commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Replaces #22 (reverted in the first commit) and #23.

Rule

Every co_await returns the caller to its context. Only schedule() moves the caller.

Changes

  • task_base has no rvalue operator co_await: it leaked the frame
  • the scheduler (now context) lives in the promise, the payload is user data only (payload_base)
  • no inject step: task awaiters read the caller's promise in await_suspend
  • affine final awaiter: a task continues its parent on the parent's context, also after an exception
  • one marker for awaiters that resume the caller themselves (this_coro_awaiter_base)
  • dev::steal only captures foreign awaitables, the other steal overloads and executor::steal are removed
  • scheduled_task -> spawn_task with its own awaiter, the spawned task gets a fresh payload
  • tags receive the context and the payload, this_coro::scheduler -> this_coro::context
  • executor::sync_wait(task, args...)
  • sync_executor_task is removed
  • copies are conditionally noexcept, new concepts mta::nothrow_constructible, mta::nothrow_copy_constructible
  • trait packs are type level (about 2.6x faster on gcc, 15% on clang), new mta::type_pack<T>; the consteval functions behind the _t aliases are removed
  • context interface, co::current_context(), scheduler final : context, strand final : context
  • dev::affine_promise<T> holds the affine final awaiter and parentContext together, one awaiter for all tasks, the promise's attach() does the per task part, raw_promise_base holds the shared promise members
  • data_awaiter moves its value out

Verification (linux)

  • cth_coro_tests 95/95 on gcc and clang, debug and release, and with ASan (0 leaks)
  • cth_tests 188/188, cth_net_tests 8/8
  • cth_win is checked by reading only, CI builds windows

… frame leak on rvalue co_await, spawn_task returns to the caller (#22)"

This reverts commit 48805f0.
- 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&
- 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
- 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
- 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
- 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
- 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
…ts 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
…e 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
- 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
…ntext, 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
- 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
@codethinki
codethinki merged commit 2e20674 into master Oct 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant