Aria 3.2.6
C++23 MVVM framework — 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<std::conditional_t<std::is_void_v<Ts>, std::monostate, 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 // Allocate every driver before launching any work. A frame allocation
145 // failure must unwind owned, unstarted Tasks instead of leaving a
146 // partially launched operation behind. These local owners also remain
147 // valid if the final child resumes and destroys this awaiter inline.
148 auto drivers = std::apply([this](auto&... task) {
149 return std::apply([this, &task...](auto&... slot) {
150 return std::make_tuple(detail::drive_one(
151 std::move(task), slot, remaining_, parent_)...);
152 }, slots_);
153 }, tasks_);
154 parent_->store(caller);
155 detail::publish_race_trace(detail::race_source::kWhenAll,
156 detail::race_op::kStart,
157 sizeof...(Ts));
158 // Spawn drivers as fully-detached coroutines. Each driver owns its
159 // own coroutine frame via Task::start_detached — no need for the
160 // awaiter to keep them alive, and no leaks.
161 std::apply([](auto&... driver) {
162 (std::move(driver).start_detached(), ...);
163 }, drivers);
164 }
165
167 // Guarded by the same condition as await_ready: a zero-task
168 // when_all never suspends, so it never armed a race and must not
169 // publish an unpaired race_end.
170 if constexpr (sizeof...(Ts) > 0) {
171 detail::publish_race_trace(detail::race_source::kWhenAll,
172 detail::race_op::kEnd);
173 }
174 std::exception_ptr first_err;
175 std::apply([&](auto&... slot) {
176 (((slot->error && !first_err) ? first_err = slot->error : nullptr), ...);
177 }, slots_);
178
179 if (first_err) std::rethrow_exception(first_err);
180 return build_result_(std::index_sequence_for<Ts...>{});
181 }
182
183private:
184 template<std::size_t... I>
185 Result build_result_(std::index_sequence<I...>) {
186 return Result{slot_result(std::get<I>(slots_))...};
187 }
188
189 template<typename T>
190 static auto slot_result(const std::shared_ptr<detail::WhenAllSlot<T>>& slot) {
191 if constexpr (std::is_void_v<T>) { return std::monostate{}; }
192 else { return std::move(*slot->value); }
193 }
194
195 std::tuple<Task<Ts>...> tasks_;
196 std::tuple<std::shared_ptr<detail::WhenAllSlot<Ts>>...> slots_;
197 std::shared_ptr<std::atomic<std::size_t>> remaining_ =
198 std::make_shared<std::atomic<std::size_t>>(sizeof...(Ts));
199 std::shared_ptr<detail::WhenAllParentHandle> parent_ =
200 std::make_shared<detail::WhenAllParentHandle>();
201};
202
203template<typename... Ts>
204auto when_all(Task<Ts>... tasks) {
205 return WhenAllAwaiter<Ts...>{std::move(tasks)...};
206}
207
208// ── when_any ────────────────────────────────────────────────────────────
209
210namespace detail {
211
216template<typename T>
217Task<void> drive_any_basic_(Task<T> task,
218 std::size_t idx,
219 std::shared_ptr<RaceSlot<T>> slot)
220{
221 try {
222 if constexpr (std::is_void_v<T>) {
223 co_await std::move(task);
224 if (slot->try_claim(/*winner=*/1)) {
225 slot->winner_index = idx;
226 slot->store_value_or_exception(); // void success
227 slot->publish(/*winner=*/1);
228 publish_race_trace(race_source::kWhenAny, race_op::kWon, idx);
229 slot->notify_winner_resume();
230 }
231 } else {
232 T v = co_await std::move(task);
233 if (slot->try_claim(/*winner=*/1)) {
234 slot->winner_index = idx;
235 slot->store_value_or_exception(std::move(v));
236 slot->publish(/*winner=*/1);
237 publish_race_trace(race_source::kWhenAny, race_op::kWon, idx);
238 slot->notify_winner_resume();
239 }
240 }
241 } catch (...) {
242 if (slot->try_claim(/*winner=*/1)) {
243 slot->winner_index = idx;
244 slot->result.template emplace<2>(std::current_exception());
245 slot->publish(/*winner=*/1);
246 // Still the winner, just with a failure; the exception
247 // surfaces through await_resume rather than as its own event.
248 publish_race_trace(race_source::kWhenAny, race_op::kWon, idx);
249 slot->notify_winner_resume();
250 }
251 }
252}
253
258template<typename T, typename Factory>
259Task<void> drive_any_cancellable_(Factory factory,
260 std::size_t idx,
261 std::shared_ptr<RaceSlot<T>> slot,
262 std::shared_ptr<CancellationSource> src,
263 std::shared_ptr<std::vector<std::shared_ptr<CancellationSource>>> all_sources)
264{
265 auto cancel_losers = [all_sources, idx]() {
266 std::uint64_t signalled = 0;
267 for (std::size_t i = 0; i < all_sources->size(); ++i) {
268 if (i == idx) continue;
269 if (auto& s = (*all_sources)[i]; s) {
270 s->cancel();
271 ++signalled;
272 }
273 }
274 // One event per race, not per loser (D-31.1): `generation`
275 // carries how many losers were actually signalled.
276 publish_race_trace(race_source::kWhenAnyCancellable,
277 race_op::kLoserCancel, signalled);
278 };
279
280 CancellationToken tok = src->token();
281
282 try {
283 if constexpr (std::is_void_v<T>) {
284 co_await factory(tok);
285 if (slot->try_claim(/*winner=*/1)) {
286 slot->winner_index = idx;
287 slot->store_value_or_exception();
288 slot->publish(/*winner=*/1);
289 publish_race_trace(race_source::kWhenAnyCancellable,
290 race_op::kWon, idx);
291 cancel_losers();
292 slot->notify_winner_resume();
293 }
294 } else {
295 T v = co_await factory(tok);
296 if (slot->try_claim(/*winner=*/1)) {
297 slot->winner_index = idx;
298 slot->store_value_or_exception(std::move(v));
299 slot->publish(/*winner=*/1);
300 publish_race_trace(race_source::kWhenAnyCancellable,
301 race_op::kWon, idx);
302 cancel_losers();
303 slot->notify_winner_resume();
304 }
305 }
306 } catch (...) {
307 if (slot->try_claim(/*winner=*/1)) {
308 slot->winner_index = idx;
309 slot->result.template emplace<2>(std::current_exception());
310 slot->publish(/*winner=*/1);
311 publish_race_trace(race_source::kWhenAnyCancellable,
312 race_op::kWon, idx);
313 cancel_losers();
314 slot->notify_winner_resume();
315 }
316 }
317}
318
319} // namespace detail
320
333template<typename T>
335public:
336 // For void T the `value` slot collapses to monostate so callers can
337 // still use a uniform `Result` shape (T-typed code paths can check
338 // `error` and `index`).
339 using ValueField = std::conditional_t<std::is_void_v<T>,
340 std::monostate,
341 std::optional<T>>;
342 struct Result {
343 std::size_t index = std::size_t(-1);
345 std::exception_ptr error{};
346 };
347
348 explicit WhenAnyAwaiter(std::vector<Task<T>> tasks) : tasks_(std::move(tasks)) {}
349
350 bool await_ready() const noexcept {
351 if (tasks_.empty()) return true;
352 return slot_->winner.load(std::memory_order_acquire) != 0;
353 }
354
355 bool await_suspend(std::coroutine_handle<> caller) {
356 detail::publish_race_trace(detail::race_source::kWhenAny,
357 detail::race_op::kStart,
358 tasks_.size());
359 // Start all driver coroutines. Each captures `slot_` so the
360 // shared race state lives as long as needed. If any driver
361 // synchronously resolves the slot, our await_suspend tail
362 // re-checks `winner` under `mu` and skips suspension.
363 for (std::size_t i = 0; i < tasks_.size(); ++i) {
364 detail::drive_any_basic_<T>(std::move(tasks_[i]), i, slot_)
365 .start_detached();
366 }
367 std::lock_guard lk(slot_->mu);
368 if (slot_->winner.load(std::memory_order_acquire) != 0) {
369 return false;
370 }
371 slot_->parent_handle = caller;
372 slot_->parent_stored = true;
373 return true;
374 }
375
377 // An empty task list short-circuits await_ready and never armed a
378 // race, so it must not publish an unpaired race_end.
379 if (!tasks_.empty()) {
380 detail::publish_race_trace(detail::race_source::kWhenAny,
381 detail::race_op::kEnd);
382 }
383 Result r;
384 r.index = slot_->winner_index;
385 auto& v = slot_->result;
386 if (v.index() == 2) {
387 r.error = std::get<2>(v);
388 } else if constexpr (!std::is_void_v<T>) {
389 if (v.index() == 1) r.value = std::get<1>(std::move(v));
390 }
391 return r;
392 }
393
394private:
395 std::vector<Task<T>> tasks_;
396 std::shared_ptr<detail::RaceSlot<T>> slot_ =
397 std::make_shared<detail::RaceSlot<T>>();
398};
399
400template<typename T>
401auto when_any(std::vector<Task<T>> tasks) {
402 return WhenAnyAwaiter<T>{std::move(tasks)};
403}
404
414template<typename T>
416public:
417 using Factory = std::function<Task<T>(CancellationToken)>;
419
420 explicit WhenAnyCancellableAwaiter(std::vector<Factory> factories)
421 : factories_(std::move(factories)) {
422 sources_->reserve(factories_.size());
423 for (std::size_t i = 0; i < factories_.size(); ++i) {
424 sources_->push_back(std::make_shared<CancellationSource>());
425 }
426 }
427
428 bool await_ready() const noexcept {
429 if (factories_.empty()) return true;
430 return slot_->winner.load(std::memory_order_acquire) != 0;
431 }
432
433 bool await_suspend(std::coroutine_handle<> caller) {
434 detail::publish_race_trace(detail::race_source::kWhenAnyCancellable,
435 detail::race_op::kStart,
436 factories_.size());
437 for (std::size_t i = 0; i < factories_.size(); ++i) {
438 detail::drive_any_cancellable_<T, Factory>(
439 std::move(factories_[i]), i, slot_, (*sources_)[i], sources_)
440 .start_detached();
441 }
442 std::lock_guard lk(slot_->mu);
443 if (slot_->winner.load(std::memory_order_acquire) != 0) {
444 return false;
445 }
446 slot_->parent_handle = caller;
447 slot_->parent_stored = true;
448 return true;
449 }
450
452 if (!factories_.empty()) {
453 detail::publish_race_trace(detail::race_source::kWhenAnyCancellable,
454 detail::race_op::kEnd);
455 }
456 Result r;
457 r.index = slot_->winner_index;
458 auto& v = slot_->result;
459 if (v.index() == 2) {
460 r.error = std::get<2>(v);
461 } else if constexpr (!std::is_void_v<T>) {
462 if (v.index() == 1) r.value = std::get<1>(std::move(v));
463 }
464 return r;
465 }
466
467private:
468 std::vector<Factory> factories_;
469 std::shared_ptr<detail::RaceSlot<T>> slot_ =
470 std::make_shared<detail::RaceSlot<T>>();
471 std::shared_ptr<std::vector<std::shared_ptr<CancellationSource>>>
472 sources_ = std::make_shared<std::vector<std::shared_ptr<CancellationSource>>>();
473};
474
475template<typename T>
477 std::vector<std::function<Task<T>(CancellationToken)>> factories) {
478 return WhenAnyCancellableAwaiter<T>{std::move(factories)};
479}
480
481} // namespace aria::async
Definition cancellation.hpp:182
Definition task.hpp:79
Awaitable that resolves when ALL input Tasks complete.
Definition when_all.hpp:133
WhenAllAwaiter(Task< Ts >... ts)
Definition when_all.hpp:137
std::tuple< std::conditional_t< std::is_void_v< Ts >, std::monostate, Ts >... > Result
Definition when_all.hpp:135
Result await_resume()
Definition when_all.hpp:166
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:334
bool await_suspend(std::coroutine_handle<> caller)
Definition when_all.hpp:355
WhenAnyAwaiter(std::vector< Task< T > > tasks)
Definition when_all.hpp:348
Result await_resume()
Definition when_all.hpp:376
std::conditional_t< std::is_void_v< T >, std::monostate, std::optional< T > > ValueField
Definition when_all.hpp:339
bool await_ready() const noexcept
Definition when_all.hpp:350
Awaitable that resolves when ANY of the input task FACTORIES completes (success or error).
Definition when_all.hpp:415
bool await_suspend(std::coroutine_handle<> caller)
Definition when_all.hpp:433
bool await_ready() const noexcept
Definition when_all.hpp:428
typename WhenAnyAwaiter< T >::Result Result
Definition when_all.hpp:418
std::function< Task< T >(CancellationToken)> Factory
Definition when_all.hpp:417
Result await_resume()
Definition when_all.hpp:451
WhenAnyCancellableAwaiter(std::vector< Factory > factories)
Definition when_all.hpp:420
Definition async_command.hpp:118
auto when_any(std::vector< Task< T > > tasks)
Definition when_all.hpp:401
auto when_all(Task< Ts >... tasks)
Definition when_all.hpp:204
auto when_any_cancellable(std::vector< std::function< Task< T >(CancellationToken)> > factories)
Definition when_all.hpp:476
Definition validation_key.hpp:110
Definition when_all.hpp:342
std::size_t index
Definition when_all.hpp:343
ValueField value
Definition when_all.hpp:344
std::exception_ptr error
Definition when_all.hpp:345