Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
when_all.hpp
Go to the documentation of this file.
1#pragma once
2
3// when_all / when_any combinators for aria::async::Task<T>.
4//
5// Usage:
6// auto [a, b, c] = co_await when_all(fetchA(), fetchB(), fetchC());
7// auto first = co_await when_any(slow(), fast(), medium());
8//
9// // Cancellable form: each factory receives a CancellationToken;
10// // losers are notified via their token once a winner emerges.
11// std::vector<std::function<Task<int>(CancellationToken)>> fs = {
12// [](CancellationToken t) -> Task<int> { co_return co_await slow_op(t); },
13// [](CancellationToken t) -> Task<int> { co_return co_await fast_op(t); },
14// };
15// auto first = co_await when_any_cancellable(std::move(fs));
16//
17// Implementation strategy:
18// when_all: each input task is started immediately and drives a shared
19// atomic counter. The combinator suspends until the counter
20// reaches the target.
21// when_any: built on detail::RaceSlot — atomic winner CAS plus a mu_-
22// serialised parent_handle so a synchronously-completing task
23// cannot resume the parent before await_suspend has finished
24// storing it.
25
26#include "aria/async/task.hpp"
28#include "aria/async/detail/race_slot.hpp"
29#include "aria/async/detail/race_trace.hpp"
30
31#include <atomic>
32#include <coroutine>
33#include <cstdint>
34#include <exception>
35#include <functional>
36#include <memory>
37#include <mutex>
38#include <optional>
39#include <tuple>
40#include <type_traits>
41#include <utility>
42#include <variant>
43#include <vector>
44
45namespace aria::async {
46
47namespace detail {
48
49template<typename T>
50struct WhenAllSlot {
51 std::optional<T> value;
52 std::exception_ptr error;
53};
54
55template<>
56struct WhenAllSlot<void> {
57 std::exception_ptr error;
58};
59
73class WhenAllParentHandle {
74public:
76 void store(std::coroutine_handle<> h) noexcept {
77 slot_.store(h.address(), std::memory_order_release);
78 }
81 [[nodiscard]] std::coroutine_handle<> load() const noexcept {
82 void* p = slot_.load(std::memory_order_acquire);
83 return p ? std::coroutine_handle<>::from_address(p)
84 : std::coroutine_handle<>{};
85 }
86
87private:
88 std::atomic<void*> slot_{nullptr};
89};
90
93template<typename T>
94Task<void> drive_one(Task<T> task,
95 std::shared_ptr<WhenAllSlot<T>> slot,
96 std::shared_ptr<std::atomic<std::size_t>> remaining,
97 std::shared_ptr<WhenAllParentHandle> parent) {
98 try {
99 if constexpr (std::is_void_v<T>) {
100 co_await std::move(task);
101 } else {
102 slot->value.emplace(co_await std::move(task));
103 }
104 } catch (...) {
105 slot->error = std::current_exception();
106 }
107 if (remaining->fetch_sub(1, std::memory_order_acq_rel) == 1) {
108 // Last participant home: when_all has no losers, so completion
109 // IS the arbitration outcome (D-31.1). A participant that threw
110 // does not change which event fires — the failure surfaces
111 // through await_resume.
112 publish_race_trace(race_source::kWhenAll, race_op::kWon);
113 if (auto h = parent->load()) h.resume();
114 }
115}
116
117} // namespace detail
118
132template<typename... Ts>
134public:
135 using Result = std::tuple<Ts...>;
136
137 explicit WhenAllAwaiter(Task<Ts>... ts)
138 : tasks_(std::make_tuple(std::move(ts)...)),
139 slots_(std::make_tuple(std::make_shared<detail::WhenAllSlot<Ts>>()...)) {}
140
141 bool await_ready() const noexcept { return sizeof...(Ts) == 0; }
142
143 void await_suspend(std::coroutine_handle<> caller) {
144 parent_->store(caller);
145 detail::publish_race_trace(detail::race_source::kWhenAll,
146 detail::race_op::kStart,
147 sizeof...(Ts));
148 // Spawn drivers as fully-detached coroutines. Each driver owns its
149 // own coroutine frame via Task::start_detached — no need for the
150 // awaiter to keep them alive, and no leaks.
151 std::apply([this](auto&... task) {
152 std::apply([this, &task...](auto&... slot) {
153 (detail::drive_one(
154 std::move(task), slot, remaining_, parent_
155 ).start_detached(), ...);
156 }, slots_);
157 }, tasks_);
158 }
159
161 // Guarded by the same condition as await_ready: a zero-task
162 // when_all never suspends, so it never armed a race and must not
163 // publish an unpaired race_end.
164 if constexpr (sizeof...(Ts) > 0) {
165 detail::publish_race_trace(detail::race_source::kWhenAll,
166 detail::race_op::kEnd);
167 }
168 std::exception_ptr first_err;
169 std::apply([&](auto&... slot) {
170 (((slot->error && !first_err) ? first_err = slot->error : nullptr), ...);
171 }, slots_);
172
173 if (first_err) std::rethrow_exception(first_err);
174 return build_result_(std::index_sequence_for<Ts...>{});
175 }
176
177private:
178 template<std::size_t... I>
179 Result build_result_(std::index_sequence<I...>) {
180 return Result{std::move(*std::get<I>(slots_)->value)...};
181 }
182
183 std::tuple<Task<Ts>...> tasks_;
184 std::tuple<std::shared_ptr<detail::WhenAllSlot<Ts>>...> slots_;
185 std::shared_ptr<std::atomic<std::size_t>> remaining_ =
186 std::make_shared<std::atomic<std::size_t>>(sizeof...(Ts));
187 std::shared_ptr<detail::WhenAllParentHandle> parent_ =
188 std::make_shared<detail::WhenAllParentHandle>();
189};
190
191template<typename... Ts>
192auto when_all(Task<Ts>... tasks) {
193 return WhenAllAwaiter<Ts...>{std::move(tasks)...};
194}
195
196// ── when_any ────────────────────────────────────────────────────────────
197
198namespace detail {
199
204template<typename T>
205Task<void> drive_any_basic_(Task<T> task,
206 std::size_t idx,
207 std::shared_ptr<RaceSlot<T>> slot)
208{
209 try {
210 if constexpr (std::is_void_v<T>) {
211 co_await std::move(task);
212 if (slot->try_claim(/*winner=*/1)) {
213 slot->winner_index = idx;
214 slot->store_value_or_exception(); // void success
215 slot->publish(/*winner=*/1);
216 publish_race_trace(race_source::kWhenAny, race_op::kWon, idx);
217 slot->notify_winner_resume();
218 }
219 } else {
220 T v = co_await std::move(task);
221 if (slot->try_claim(/*winner=*/1)) {
222 slot->winner_index = idx;
223 slot->store_value_or_exception(std::move(v));
224 slot->publish(/*winner=*/1);
225 publish_race_trace(race_source::kWhenAny, race_op::kWon, idx);
226 slot->notify_winner_resume();
227 }
228 }
229 } catch (...) {
230 if (slot->try_claim(/*winner=*/1)) {
231 slot->winner_index = idx;
232 slot->result.template emplace<2>(std::current_exception());
233 slot->publish(/*winner=*/1);
234 // Still the winner, just with a failure; the exception
235 // surfaces through await_resume rather than as its own event.
236 publish_race_trace(race_source::kWhenAny, race_op::kWon, idx);
237 slot->notify_winner_resume();
238 }
239 }
240}
241
246template<typename T, typename Factory>
247Task<void> drive_any_cancellable_(Factory factory,
248 std::size_t idx,
249 std::shared_ptr<RaceSlot<T>> slot,
250 std::shared_ptr<CancellationSource> src,
251 std::shared_ptr<std::vector<std::shared_ptr<CancellationSource>>> all_sources)
252{
253 auto cancel_losers = [all_sources, idx]() {
254 std::uint64_t signalled = 0;
255 for (std::size_t i = 0; i < all_sources->size(); ++i) {
256 if (i == idx) continue;
257 if (auto& s = (*all_sources)[i]; s) {
258 s->cancel();
259 ++signalled;
260 }
261 }
262 // One event per race, not per loser (D-31.1): `generation`
263 // carries how many losers were actually signalled.
264 publish_race_trace(race_source::kWhenAnyCancellable,
265 race_op::kLoserCancel, signalled);
266 };
267
268 CancellationToken tok = src->token();
269
270 try {
271 if constexpr (std::is_void_v<T>) {
272 co_await factory(tok);
273 if (slot->try_claim(/*winner=*/1)) {
274 slot->winner_index = idx;
275 slot->store_value_or_exception();
276 slot->publish(/*winner=*/1);
277 publish_race_trace(race_source::kWhenAnyCancellable,
278 race_op::kWon, idx);
279 cancel_losers();
280 slot->notify_winner_resume();
281 }
282 } else {
283 T v = co_await factory(tok);
284 if (slot->try_claim(/*winner=*/1)) {
285 slot->winner_index = idx;
286 slot->store_value_or_exception(std::move(v));
287 slot->publish(/*winner=*/1);
288 publish_race_trace(race_source::kWhenAnyCancellable,
289 race_op::kWon, idx);
290 cancel_losers();
291 slot->notify_winner_resume();
292 }
293 }
294 } catch (...) {
295 if (slot->try_claim(/*winner=*/1)) {
296 slot->winner_index = idx;
297 slot->result.template emplace<2>(std::current_exception());
298 slot->publish(/*winner=*/1);
299 publish_race_trace(race_source::kWhenAnyCancellable,
300 race_op::kWon, idx);
301 cancel_losers();
302 slot->notify_winner_resume();
303 }
304 }
305}
306
307} // namespace detail
308
321template<typename T>
323public:
324 // For void T the `value` slot collapses to monostate so callers can
325 // still use a uniform `Result` shape (T-typed code paths can check
326 // `error` and `index`).
327 using ValueField = std::conditional_t<std::is_void_v<T>,
328 std::monostate,
329 std::optional<T>>;
330 struct Result {
331 std::size_t index = std::size_t(-1);
333 std::exception_ptr error{};
334 };
335
336 explicit WhenAnyAwaiter(std::vector<Task<T>> tasks) : tasks_(std::move(tasks)) {}
337
338 bool await_ready() const noexcept {
339 if (tasks_.empty()) return true;
340 return slot_->winner.load(std::memory_order_acquire) != 0;
341 }
342
343 bool await_suspend(std::coroutine_handle<> caller) noexcept {
344 detail::publish_race_trace(detail::race_source::kWhenAny,
345 detail::race_op::kStart,
346 tasks_.size());
347 // Start all driver coroutines. Each captures `slot_` so the
348 // shared race state lives as long as needed. If any driver
349 // synchronously resolves the slot, our await_suspend tail
350 // re-checks `winner` under `mu` and skips suspension.
351 for (std::size_t i = 0; i < tasks_.size(); ++i) {
352 detail::drive_any_basic_<T>(std::move(tasks_[i]), i, slot_)
353 .start_detached();
354 }
355 std::lock_guard lk(slot_->mu);
356 if (slot_->winner.load(std::memory_order_acquire) != 0) {
357 return false;
358 }
359 slot_->parent_handle = caller;
360 slot_->parent_stored = true;
361 return true;
362 }
363
365 // An empty task list short-circuits await_ready and never armed a
366 // race, so it must not publish an unpaired race_end.
367 if (!tasks_.empty()) {
368 detail::publish_race_trace(detail::race_source::kWhenAny,
369 detail::race_op::kEnd);
370 }
371 Result r;
372 r.index = slot_->winner_index;
373 auto& v = slot_->result;
374 if (v.index() == 2) {
375 r.error = std::get<2>(v);
376 } else if constexpr (!std::is_void_v<T>) {
377 if (v.index() == 1) r.value = std::get<1>(std::move(v));
378 }
379 return r;
380 }
381
382private:
383 std::vector<Task<T>> tasks_;
384 std::shared_ptr<detail::RaceSlot<T>> slot_ =
385 std::make_shared<detail::RaceSlot<T>>();
386};
387
388template<typename T>
389auto when_any(std::vector<Task<T>> tasks) {
390 return WhenAnyAwaiter<T>{std::move(tasks)};
391}
392
402template<typename T>
404public:
405 using Factory = std::function<Task<T>(CancellationToken)>;
407
408 explicit WhenAnyCancellableAwaiter(std::vector<Factory> factories)
409 : factories_(std::move(factories)) {
410 sources_->reserve(factories_.size());
411 for (std::size_t i = 0; i < factories_.size(); ++i) {
412 sources_->push_back(std::make_shared<CancellationSource>());
413 }
414 }
415
416 bool await_ready() const noexcept {
417 if (factories_.empty()) return true;
418 return slot_->winner.load(std::memory_order_acquire) != 0;
419 }
420
421 bool await_suspend(std::coroutine_handle<> caller) noexcept {
422 detail::publish_race_trace(detail::race_source::kWhenAnyCancellable,
423 detail::race_op::kStart,
424 factories_.size());
425 for (std::size_t i = 0; i < factories_.size(); ++i) {
426 detail::drive_any_cancellable_<T, Factory>(
427 std::move(factories_[i]), i, slot_, (*sources_)[i], sources_)
428 .start_detached();
429 }
430 std::lock_guard lk(slot_->mu);
431 if (slot_->winner.load(std::memory_order_acquire) != 0) {
432 return false;
433 }
434 slot_->parent_handle = caller;
435 slot_->parent_stored = true;
436 return true;
437 }
438
440 if (!factories_.empty()) {
441 detail::publish_race_trace(detail::race_source::kWhenAnyCancellable,
442 detail::race_op::kEnd);
443 }
444 Result r;
445 r.index = slot_->winner_index;
446 auto& v = slot_->result;
447 if (v.index() == 2) {
448 r.error = std::get<2>(v);
449 } else if constexpr (!std::is_void_v<T>) {
450 if (v.index() == 1) r.value = std::get<1>(std::move(v));
451 }
452 return r;
453 }
454
455private:
456 std::vector<Factory> factories_;
457 std::shared_ptr<detail::RaceSlot<T>> slot_ =
458 std::make_shared<detail::RaceSlot<T>>();
459 std::shared_ptr<std::vector<std::shared_ptr<CancellationSource>>>
460 sources_ = std::make_shared<std::vector<std::shared_ptr<CancellationSource>>>();
461};
462
463template<typename T>
465 std::vector<std::function<Task<T>(CancellationToken)>> factories) {
466 return WhenAnyCancellableAwaiter<T>{std::move(factories)};
467}
468
469} // namespace aria::async
Definition cancellation.hpp:182
Definition task.hpp:78
Awaitable that resolves when ALL input Tasks complete.
Definition when_all.hpp:133
std::tuple< Ts... > Result
Definition when_all.hpp:135
WhenAllAwaiter(Task< Ts >... ts)
Definition when_all.hpp:137
Result await_resume()
Definition when_all.hpp:160
void await_suspend(std::coroutine_handle<> caller)
Definition when_all.hpp:143
bool await_ready() const noexcept
Definition when_all.hpp:141
Awaitable that resolves when ANY of the input Tasks completes (success or error).
Definition when_all.hpp:322
WhenAnyAwaiter(std::vector< Task< T > > tasks)
Definition when_all.hpp:336
bool await_suspend(std::coroutine_handle<> caller) noexcept
Definition when_all.hpp:343
Result await_resume()
Definition when_all.hpp:364
std::conditional_t< std::is_void_v< T >, std::monostate, std::optional< T > > ValueField
Definition when_all.hpp:327
bool await_ready() const noexcept
Definition when_all.hpp:338
Awaitable that resolves when ANY of the input task FACTORIES completes (success or error).
Definition when_all.hpp:403
bool await_suspend(std::coroutine_handle<> caller) noexcept
Definition when_all.hpp:421
bool await_ready() const noexcept
Definition when_all.hpp:416
typename WhenAnyAwaiter< T >::Result Result
Definition when_all.hpp:406
std::function< Task< T >(CancellationToken)> Factory
Definition when_all.hpp:405
Result await_resume()
Definition when_all.hpp:439
WhenAnyCancellableAwaiter(std::vector< Factory > factories)
Definition when_all.hpp:408
Definition async_command.hpp:118
auto when_any(std::vector< Task< T > > tasks)
Definition when_all.hpp:389
auto when_all(Task< Ts >... tasks)
Definition when_all.hpp:192
auto when_any_cancellable(std::vector< std::function< Task< T >(CancellationToken)> > factories)
Definition when_all.hpp:464
Definition validation_key.hpp:110
Definition when_all.hpp:330
std::size_t index
Definition when_all.hpp:331
ValueField value
Definition when_all.hpp:332
std::exception_ptr error
Definition when_all.hpp:333