⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@mvandeberg
Copy link
Contributor

@mvandeberg mvandeberg commented Jan 21, 2026

Closes #98

Summary by CodeRabbit

  • New Features

    • Added when_any coroutine utility (heterogeneous and vector variants) to race tasks and return the first completion; new public types/APIs exposed.
  • Documentation

    • New comprehensive when_any guide with examples and comparisons.
    • Navigation updated to include "Racing Tasks" and when_all guidance now references when_any.
  • Tests

    • Added extensive unit tests and updated test utilities/executor support for when_any scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds a new when_any facility (heterogeneous and vector forms) with public APIs, umbrella header exposure, documentation and nav updates, extensive unit tests, a safety comment in when_all, and consolidated test helpers.

Changes

Cohort / File(s) Summary
Documentation: Navigation & Pages
doc/modules/ROOT/nav.adoc, doc/modules/ROOT/pages/coroutines/when-all.adoc
Added "Racing Tasks" nav entry and updated when_all guidance/next-steps to reference the new when_any docs.
Documentation: New Feature Page
doc/modules/ROOT/pages/coroutines/when-any.adoc
New comprehensive when_any documentation describing semantics, return types, exception/cancellation behavior, examples, comparisons, and next steps.
Core Implementation: when_any facility
include/boost/capy/when_any.hpp
New implementation providing heterogeneous (variadic tuple/variant) and homogeneous (vector) when_any APIs, state coordination, per-task runners, launchers, result/exception propagation, cancellation handling, and public type aliases.
Header Integration
include/boost/capy.hpp
Umbrella header now includes \<boost/capy/when_any.hpp>`` to expose when_any.
Implementation Note
include/boost/capy/when_all.hpp
Added a critical lifetime-safety comment about synchronous completion when launching the last task.
Unit Tests: when_any
test/unit/when_any.cpp
New exhaustive unit tests covering heterogeneous and vector use-cases, winner selection, exception propagation, stop-token semantics, nested combinators, and edge cases.
Test Utilities Added / Centralized
test/unit/test_helpers.hpp
Added test_exception, throw_test_exception, helper tasks (returns_int, returns_string, void_task, throws_exception, void_throws_exception), queuing_executor, and yield_awaitable for tests.
Test Adjustments: removed helpers
test/unit/task.cpp, test/unit/when_all.cpp
Removed previously-local queuing_executor, test_exception, and helper-task definitions (now centralized in test_helpers.hpp).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Launcher as when_any_launcher
    participant State as when_any_state
    participant Runner0 as when_any_runner[0]
    participant Runner1 as when_any_runner[1]
    participant Task0 as task[0]
    participant Task1 as task[1]

    Client->>Launcher: call when_any(task0, task1)
    Launcher->>State: create shared state (remaining=N)
    Launcher->>Runner0: spawn runner for task0
    Launcher->>Runner1: spawn runner for task1

    par concurrent
        Runner0->>Task0: co_await inner
        Runner1->>Task1: co_await inner
    end

    alt Task0 completes first
        Task0->>Runner0: complete (result/exception)
        Runner0->>State: try_win(index=0)
        State->>State: set winner, store result/exception
        Runner0->>State: signal_completion()
    else Task1 completes first
        Task1->>Runner1: complete (result/exception)
        Runner1->>State: try_win(index=1)
        State->>State: set winner, store result/exception
        Runner1->>State: signal_completion()
    end

    Note over State: Non-winning tasks may be cancelled or ignored
    State->>Launcher: all runners completed
    Launcher->>Client: return (winner_index, winner_result) or rethrow winner exception
Loading

Estimated Code Review Effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped through headers, raced tasks with delight,
I spotted the first finisher and held it tight,
An index, a variant, the victor was shown,
I tucked the rest gently and carried it home. 🌿

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature: when_any' is concise and directly summarizes the main feature being implemented, matching the PR objectives.
Linked Issues check ✅ Passed The PR implements the when_any combinator as required by issue #98, including heterogeneous and homogeneous variants with comprehensive documentation and tests.
Out of Scope Changes check ✅ Passed All changes are in scope: when_any implementation, documentation, tests, and necessary refactoring (moving test utilities to test_helpers.hpp) directly support the feature requirement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@include/boost/capy/when_any.hpp`:
- Line 7: Update the header comment repository URL in
include/boost/capy/when_any.hpp to point to the correct "capy" repository
(replace the incorrect https://github.com/cppalliance/corosio link with the
project's actual capy repo URL used elsewhere in this project), by editing the
top-of-file comment so it references the canonical capy repository URL.
- Around line 447-449: When a coroutine wins (state->try_win(Index)) the
subsequent call to state->set_winner_result(std::move(result)) may throw and its
exception would be lost; update both winner paths (the block using auto result =
co_await std::move(inner); and the second location) to wrap
set_winner_result(...) in a try/catch, and on catch call
state->set_winner_exception(std::current_exception()) so exceptions are recorded
instead of being dropped; locate the calls to try_win, set_winner_result and add
the try/catch that invokes set_winner_exception on error.
- Around line 153-156: The instantiation of std::variant<> inside
unique_variant_t is ill-formed; change the accumulator to start from the first
pack type so the variant is always well-formed: modify unique_variant_t to
initialize deduplicate_impl with std::variant<void_to_monostate_t<T0>> (i.e.,
use the first pack element) and then pass the remaining
void_to_monostate_t<Ts>... to deduplicate_impl so the accumulator variant is
constructed from a real type; reference unique_variant_t, deduplicate_impl and
void_to_monostate_t when making this change.

In `@test/unit/when_any.cpp`:
- Line 7: Replace the incorrect repository URL comment in test/unit/when_any.cpp
that references "https://github.com/cppalliance/corosio" with the correct capy
repository URL used by this project; locate the top-of-file comment (the
repository URL string) and update it to the project's canonical URL (or remove
the stray copy-paste comment) so the file no longer points to corosio.

@cppalliance-bot
Copy link

cppalliance-bot commented Jan 21, 2026

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

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

2026-01-23 22:28:08 UTC

@cppalliance-bot
Copy link

cppalliance-bot commented Jan 21, 2026

GCOVR code coverage report https://101.capy.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://101.capy.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff Report (in development) https://101.capy.prtest3.cppalliance.org/diff-report/index.html

Build time: 2026-01-23 22:34:29 UTC

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@include/boost/capy/when_any.hpp`:
- Around line 1288-1293: The homogenous branch after co_await inner must mirror
the heterogeneous runner's exception handling: after calling
state->try_win(index) and before/around
state->set_winner_result(std::move(result)), wrap the set_winner_result call in
a try/catch; on catch(...) capture the exception via std::current_exception()
and forward it to the same error path used elsewhere (e.g., call
state->set_winner_exception(...) or the equivalent error-setter used by the
heterogeneous make_when_any_runner), ensuring you still only call the setter
when try_win(index) succeeded and that exceptions from set_winner_result are
propagated into the state's exception storage.
🧹 Nitpick comments (2)
include/boost/capy/when_any.hpp (1)

658-661: Consider edge case: await_ready may never return true.

The condition sizeof...(Ts) == 0 will never be true at runtime since when_any requires at least one task (T0 in the template). This makes await_ready effectively always return false, which is fine but the comment "degenerate case" is somewhat misleading. The variadic when_any enforces at least one argument at the API level.

test/unit/when_any.cpp (1)

596-600: Minor: Consider using std::deque for more efficient front removal.

The work queue processing pattern uses erase(begin()) on a vector, which is O(n) for each removal. For large queue sizes, consider using std::deque instead for O(1) front removal.

♻️ Optional improvement
-        std::vector<coro> work_queue;
+        std::deque<coro> work_queue;
         queuing_executor ex(work_queue);
         ...
         while (!work_queue.empty()) {
             auto h = work_queue.front();
-            work_queue.erase(work_queue.begin());
+            work_queue.pop_front();
             h.resume();
         }

This would require updating queuing_executor to use std::deque<coro>* instead of std::vector<coro>*.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@include/boost/capy/when_any.hpp`:
- Around line 248-363: The member variant_type result_ is default-initialized
which forces the first alternative T0 to be default-constructible; change
storage to be lazy by replacing result_ with std::optional<variant_type> (e.g.
result_opt_), initialize it empty, and emplace the variant only when the winner
stores a value: update set_winner_result<T> to emplace
result_opt_.emplace().template emplace<T>(...) and set_winner_void() to
result_opt_.emplace().template emplace<std::monostate>(...), and update any
accessors/readers to use *result_opt_ (or check for engagement) so
when_any_state and try_win logic no longer requires T0 to be
default-constructible.

@mvandeberg
Copy link
Contributor Author

@coderabbitai generate docstrings

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #103

coderabbitai bot added a commit that referenced this pull request Jan 22, 2026
Docstrings generation was requested by @mvandeberg.

* #101 (comment)

The following files were modified:

* `include/boost/capy/when_all.hpp`
* `include/boost/capy/when_any.hpp`
* `test/unit/when_any.cpp`
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@include/boost/capy/when_any.hpp`:
- Around line 965-969: The homogeneous set_winner_result currently assigns into
result_ (result_ = std::move(value)) which imposes MoveAssignable; change it to
construct in-place using result_.emplace(std::move(value)) and relax the
noexcept to match construction (e.g.
noexcept(std::is_nothrow_move_constructible_v<T>)) so
move-constructible-but-non-assignable T is accepted just like the heterogeneous
path.
- Around line 822-825: Introduce an atomic flag winner_ready_ in the shared
state and use it to establish a release-acquire synchronization: when you
nominate a winner (the code path that writes state.result_,
state.winner_exception_, and state.winner_index_ and then calls dispatch()),
store winner_ready_.store(true, std::memory_order_release) immediately after
those non-atomic writes; on the when_any read side (inside when_any before
reading state.result_ or state.winner_exception_), perform
if(!winner_ready_.load(std::memory_order_acquire)) then wait/loop or otherwise
ensure acquisition before accessing those fields so the reads see the published
winner data even if a non-winner performed the final
remaining_count_.fetch_sub(acq_rel); also update the existing memory-model
comment (around the previous lines 82–87) to document this release on winner
write and acquire on winner read and why it is needed for correct
synchronization with remaining_count_.fetch_sub(acq_rel), referencing
remaining_count_.fetch_sub(acq_rel), dispatch(), when_any, winner_ready_,
state.result_, state.winner_exception_, and winner_index_.

Comment on lines +822 to +845
if(state.winner_exception_)
std::rethrow_exception(state.winner_exception_);

co_return result_type{state.winner_index_, std::move(*state.result_)};
Copy link

@coderabbitai coderabbitai bot Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "when_any.hpp" -type f

Repository: cppalliance/capy

Length of output: 93


🏁 Script executed:

# Get file size and examine the relevant section
wc -l include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 96


🏁 Script executed:

# Read the context around lines 822-825
sed -n '810,835p' include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 930


🏁 Script executed:

# Find the when_any_state definition and try_win() method
sed -n '1,100p' include/boost/capy/when_any.hpp | head -100

Repository: cppalliance/capy

Length of output: 3548


🏁 Script executed:

# Search for when_any_state struct definition
grep -n "struct when_any_state" include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 85


🏁 Script executed:

# Search for try_win method and remaining_count_
rg -n "try_win|remaining_count_" include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 1646


🏁 Script executed:

# Read the when_any_state definition around line 223
sed -n '223,380p' include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 5248


🏁 Script executed:

# Read the try_win implementation
sed -n '329,350p' include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 784


🏁 Script executed:

# Read the signal_completion implementation
sed -n '378,395p' include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 676


🏁 Script executed:

# Look for the launcher/co_await logic to see how parent waits
rg -n "when_any_launcher" include/boost/capy/when_any.hpp -A 5 | head -40

Repository: cppalliance/capy

Length of output: 1255


🏁 Script executed:

# Read lines 82-87 for the full memory model comment
sed -n '40,100p' include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 2502


🏁 Script executed:

# Let me look at how co_await awaits and what happens at that point
sed -n '680,750p' include/boost/capy/when_any.hpp

Repository: cppalliance/capy

Length of output: 2540


🏁 Script executed:

# Check the await_suspend implementation
rg -n "await_suspend|await_ready|await_resume" include/boost/capy/when_any.hpp -B 2 -A 8

Repository: cppalliance/capy

Length of output: 8959


🏁 Script executed:

# Look for executor dispatch to understand synchronization
rg -n "dispatch" include/boost/capy/when_any.hpp -B 2 -A 4

Repository: cppalliance/capy

Length of output: 3969


🌐 Web query:

C++ std::memory_order_acq_rel fetch_sub release sequence synchronization multiple threads atomics

💡 Result:

Short answer: A read-modify-write like atomic.fetch_sub(..., std::memory_order_acq_rel) is both a release and an acquire (RMW with acq_rel). As an RMW it can be part of a release-sequence (release-store head + zero or more intervening RMWs of any memory_order). An acquire that observes a value produced anywhere in that release-sequence (including a later fetch_sub) synchronizes-with the release head, so the usual release→acquire happens-before guarantees propagate through those RMWs. In other words, fetch_sub(acq_rel) can both publish prior writes (release side) and establish visibility of earlier releases when observed (acquire side), and it participates in release-sequence synchronization. [1][2][3]

Sources:

  • cppreference: memory_order and description of acq_rel and release-acquire semantics. [1]
  • Explanations of “release sequence” and RMWs (fetch_*/CAS) being included in the release sequence even if relaxed, and how an acquire that reads a value from the sequence synchronizes-with the release head. [2][3]

🏁 Script executed:

# Check executor_ref dispatch implementation
fd "executor_ref" -type f

Repository: cppalliance/capy

Length of output: 230


🏁 Script executed:

# Look at how dispatch is implemented
rg -n "dispatch" include/boost/capy/ex/ -A 5 | head -50

Repository: cppalliance/capy

Length of output: 3142


🏁 Script executed:

# Check if there are any documented synchronization guarantees
rg -n "memory" include/boost/capy/ex/ -B 2 -A 2

Repository: cppalliance/capy

Length of output: 2525


🌐 Web query:

C++ coroutine co_await resume synchronization memory ordering happens-before

💡 Result:

Short answer

  • Suspending a coroutine at co_await fully stops its execution and the awaiter object is considered no longer used by the suspending thread; that lets await_suspend() publish the coroutine_handle to another thread without holding a lock. [1][2]
  • That publication itself does not magically create C++ memory-order happens‑before edges for non-atomic writes; you must publish the handle using proper synchronization (e.g. store it into an atomic with release semantics or behind a mutex) and the thread that receives it must acquire/lock before calling resume(). Doing that establishes the required happens‑before so the resumed coroutine sees prior writes. cppreference explicitly recommends at least release on the publisher and acquire on the resumer. [1]
  • Resuming the coroutine (handle.resume()) executes the coroutine body on the resumer thread; concurrent resume from multiple threads is UB (data‑race). [1][3]

Why (brief)

  • The language guarantees the coroutine is "fully suspended" at the suspension point, which means the suspending thread can safely hand the handle to another thread as an operation; but that is a logical/thread-model guarantee, not a substitute for memory ordering between threads. To make written state visible across threads you still need the usual C++ synchronization idioms (atomics or mutexes) so that the publisher’s writes happen‑before the resume. [1][2]

Practical recipe

  • In await_suspend(): prepare state, then publish the coroutine_handle using an atomic store(release) or push it onto a thread‑safe queue protected by a mutex.
  • In the worker/completion thread: load the handle with acquire (or lock), then call handle.resume().
  • Never let two threads call resume() on the same suspended handle concurrently. [1][2][3]

Sources

  • cppreference: Coroutines (notes on await_suspend, publishing handle, and memory-safety guidance). [1]
  • Lewis Baker: “Understanding operator co_await” (explanation of suspension and synchronization‑free patterns). [2]
  • Related discussion (safety/UB concerns when resuming from multiple threads). [3]

🌐 Web query:

C++ std::memory_order_acq_rel release sequence non-atomic data synchronization memory visibility

💡 Result:

Short answer

  • A read-modify-write (RMW) operation with memory_order_acq_rel acts both as a release (its store side) and an acquire (its load side). That makes it usable to publish prior non-atomic writes (release side) and to see prior writes from another thread (acquire side). [1]

Release sequence and visibility (key points)

  1. A "release sequence" is a maximal contiguous subsequence in the modification order of an atomic object whose head is a release operation; subsequent members must be either performed by the same thread as the head or be atomic RMW operations. An acquire load that reads a value from any member of that release sequence "synchronizes‑with" the head release. [2][3]
  2. When synchronization is established (release → acquire via the same atomic or via the release sequence), all writes (including non-atomic writes) that were sequenced‑before the release become visible to the acquiring thread. In other words: a release publish + an acquire read that observes that published value (or a value from the release sequence) gives the acquiring thread a guaranteed view of the publisher's prior non‑atomic stores. [1][2]
  3. Important subtlety: only RMW operations can extend a release sequence — ordinary relaxed or non‑RMW stores do not — so a later non‑RMW store in the modification order does not by itself give the acquire synchronization back to an earlier release. On many real CPUs an acquire will appear to sync with earlier releases, but the C++ memory model requires the release‑sequence rule for portability. [3][1]

Concise example

  • Thread A:
    write non‑atomic data D;
    x.store(1, memory_order_release);
  • Thread B:
    if (x.fetch_add(0, memory_order_acq_rel) /* or load(memory_order_acquire) that sees 1 */) {
    // after the acquire, B is guaranteed to see D
    }
    Here fetch_add (an RMW) or an acquire load that reads from the release sequence establishes synchronization so B sees D. [1][2]

References

  • memory_order (cppreference) — release/acquire/acq_rel and release‑acquire semantics. [1]
  • C++ standard / WG21 paper describing release sequence and synchronizes‑with (intro.races / release sequence). [2]
  • Discussion and examples (explain subtleties about which operations extend a release sequence). [3]

Synchronization data race: non-winner completion breaks release-acquire chain for winner state.

When a non-winner is the last task to complete, result_ and winner_exception_ (non-atomic) are read without synchronization. The remaining_count_.fetch_sub(acq_rel) extends the release sequence, but since the non-winner does not write these fields, the parent coroutine's resumption via dispatch() lacks explicit acquire semantics at the C++ level. Introduce winner_ready_ atomic, set with release after storing winner result/exception/void, and acquire it before reading in when_any. Update the memory-model comment at lines 82-87 to reflect this requirement.

🧵 Suggested fix (read side)
-    if(state.winner_exception_)
+    state.winner_ready_.load(std::memory_order_acquire);
+    if(state.winner_exception_)
         std::rethrow_exception(state.winner_exception_);

     co_return result_type{state.winner_index_, std::move(*state.result_)};
🧵 Suggested fix (state side)
 struct when_any_state
 {
+    std::atomic<bool> winner_ready_{false};
     ...
     template<typename T>
     void set_winner_result(T value)
         noexcept(std::is_nothrow_move_constructible_v<T>)
     {
         result_.emplace(std::in_place_type<T>, std::move(value));
+        winner_ready_.store(true, std::memory_order_release);
     }

     void set_winner_void() noexcept
     {
         result_.emplace(std::in_place_type<std::monostate>, std::monostate{});
+        winner_ready_.store(true, std::memory_order_release);
     }

     void set_winner_exception(std::exception_ptr ep) noexcept
     {
         winner_exception_ = ep;
+        winner_ready_.store(true, std::memory_order_release);
     }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(state.winner_exception_)
std::rethrow_exception(state.winner_exception_);
co_return result_type{state.winner_index_, std::move(*state.result_)};
state.winner_ready_.load(std::memory_order_acquire);
if(state.winner_exception_)
std::rethrow_exception(state.winner_exception_);
co_return result_type{state.winner_index_, std::move(*state.result_)};
🤖 Prompt for AI Agents
In `@include/boost/capy/when_any.hpp` around lines 822 - 825, Introduce an atomic
flag winner_ready_ in the shared state and use it to establish a release-acquire
synchronization: when you nominate a winner (the code path that writes
state.result_, state.winner_exception_, and state.winner_index_ and then calls
dispatch()), store winner_ready_.store(true, std::memory_order_release)
immediately after those non-atomic writes; on the when_any read side (inside
when_any before reading state.result_ or state.winner_exception_), perform
if(!winner_ready_.load(std::memory_order_acquire)) then wait/loop or otherwise
ensure acquisition before accessing those fields so the reads see the published
winner data even if a non-winner performed the final
remaining_count_.fetch_sub(acq_rel); also update the existing memory-model
comment (around the previous lines 82–87) to document this release on winner
write and acquire on winner read and why it is needed for correct
synchronization with remaining_count_.fetch_sub(acq_rel), referencing
remaining_count_.fetch_sub(acq_rel), dispatch(), when_any, winner_ready_,
state.result_, state.winner_exception_, and winner_index_.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude disagrees.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mvandeberg Could you share Claude's specific reasoning? I'd like to understand the disagreement to provide a more targeted analysis.

Here's my understanding of the synchronization concern:

The Question: When the parent coroutine reads result_ and winner_exception_, are those reads guaranteed to see the winner's writes?

The Synchronization Chain:

  1. Winner task writes to result_/winner_exception_ (non-atomic)
  2. Winner calls signal_completion()fetch_sub(acq_rel) on remaining_count_
  3. Last task (possibly different) calls signal_completion()fetch_sub(acq_rel) sees count==1
  4. Last task dispatches parent via caller_ex_.dispatch(continuation_)
  5. Parent resumes and reads result_/winner_exception_

Key Point: When a release and acquire synchronize on the same atomic, "all memory writes (including non-atomic and relaxed atomic) that happened-before the atomic store from the point of view of thread A, become visible side-effects in thread B" if "B actually returns the value that A stored, or a value from later in the release sequence."

The winner's fetch_sub(release) happens before the last task's fetch_sub(acquire) (guaranteed by counter semantics). A "release sequence" is formed when "some atomic is store-released and several other threads perform read-modify-write operations on that atomic", which should establish synchronization.

However, the concern is whether this synchronization transitively reaches the parent through dispatch(). If the executor's dispatch() doesn't provide its own memory ordering guarantees, there might be a gap between the last task's synchronization point and the parent's actual execution.

Would adding explicit synchronization via winner_ready_ be overly conservative, or is it necessary for portability across all executor implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synchronization analysis:

  • All fetch_sub operations on remaining_count_ form a release sequence
  • Winner's fetch_sub releases; subsequent fetch_sub operations participate in the modification order of remaining_count_
  • Last task's fetch_sub(acq_rel) synchronizes-with prior releases in the modification order, establishing happens-before from winner's writes
  • Executor dispatch() is expected to provide queue-based synchronization (release-on-post, acquire-on-execute) completing the chain to parent
  • Even inline executors work (same thread = sequenced-before)

Alternative considered: Adding winner_ready_ atomic (set with release after storing winner data, acquired before reading) would make synchronization self-contained and not rely on executor implementation details. Current approach is correct but requires careful reasoning about release sequences and executor behavior.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mvandeberg Thank you for sharing Claude's detailed analysis. After reviewing it carefully, I agree that the synchronization is sound.

Why the current approach is correct:

  1. Release sequence semantics: The winner's fetch_sub(acq_rel) releases the non-atomic writes to result_/winner_exception_. Subsequent fetch_sub operations (including by non-winners) form a release sequence on remaining_count_.

  2. Transitive synchronization: When the last task calls fetch_sub(acq_rel) and sees remaining == 1, it synchronizes-with all prior releases in the modification order, including the winner's. This establishes the happens-before relationship from the winner's writes.

  3. Executor guarantees: The dispatch() call is reasonably expected to provide queue-based synchronization (release-on-post, acquire-on-execute), completing the chain to the parent coroutine. For inline executors, same-thread execution provides sequenced-before ordering.

My initial concern was overly conservative. The pattern relies on well-understood release sequence behavior plus standard executor semantics, both of which are solid foundations.

Claude's note about the winner_ready_ alternative making synchronization self-contained is valid—it would eliminate dependency on executor implementation details—but given that proper memory ordering is a fundamental requirement for any executor, the current approach is both correct and appropriately efficient.

🐰


✏️ Learnings added
Learnt from: mvandeberg
Repo: cppalliance/capy PR: 101
File: include/boost/capy/when_any.hpp:842-845
Timestamp: 2026-01-22T18:13:22.758Z
Learning: In when_any implementations using atomic remaining_count_, the winner's fetch_sub(acq_rel) releases non-atomic result/exception writes, and the last task's fetch_sub(acq_rel) acquires via release sequence semantics, establishing happens-before for the parent's reads when combined with executor dispatch() synchronization guarantees.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@include/boost/capy/when_any.hpp`:
- Around line 985-989: The noexcept on set_winner_result(T value) is wrong:
replace the current noexcept condition that references
is_nothrow_move_assignable_v<std::optional<T>> with a check for
is_nothrow_move_constructible_v<T> (matching the heterogeneous overload and the
fact you call result_.emplace(std::move(value))). Update the noexcept specifier
on the set_winner_result function to use std::is_nothrow_move_constructible_v<T>
so the exception guarantee correctly reflects constructing the stored T.

@cppalliance cppalliance deleted a comment from coderabbitai bot Jan 22, 2026
@mvandeberg
Copy link
Contributor Author

@coderabbitai Do not consider when_all.hpp for docstring coverage of this PR.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Tip

For best results, initiate chat on the files or code changes.

@mvandeberg: Understood. I will exclude include/boost/capy/when_all.hpp from docstring coverage considerations for this PR, since the changes there are minimal (comment improvements only) and the PR's focus is on the new when_any feature implementation.

@mvandeberg
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

fetch_from_primary(),
fetch_from_backup()
);
// index is 0 or 1 (which task won)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If result is a variand, it already contains the index, and the index is redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only the case if each task has a different return type. As soon as tasks share return types, the index is required to know which task completed.

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.

Implement when_any

3 participants