Skip to content

Commit db92ccb

Browse files
committed
Update docs for io_result-based when_all/when_any signatures (#244)
All documentation and Antora example pages still described the old API where when_all/when_any accepted plain task<T> and returned tuples/variants without error_code. Update to match the current io_result-aware combinators: - when_all children must be io_task, result is io_result<R1,...,Rn> - when_any result is variant<error_code, R1,...,Rn> with error at index 0; only !ec wins - Replace monostate references with tuple<> for void tasks - Replace fictitious timeout_after examples with the real timeout combinator - Fix when_any docs that incorrectly described exceptions as valid completions
1 parent bf08032 commit db92ccb

File tree

10 files changed

+416
-319
lines changed

10 files changed

+416
-319
lines changed

doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc

Lines changed: 87 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -34,82 +34,95 @@ task<> concurrent()
3434

3535
== when_all: Wait for All Tasks
3636

37-
`when_all` launches multiple tasks concurrently and waits for all of them to complete:
37+
`when_all` launches multiple `io_task` children concurrently and waits for all of them to complete. It returns `task<io_result<R1, R2, ..., Rn>>`, a single `ec` plus the flattened payloads:
3838

3939
[source,cpp]
4040
----
4141
#include <boost/capy/when_all.hpp>
4242
43-
task<int> fetch_a() { co_return 1; }
44-
task<int> fetch_b() { co_return 2; }
45-
task<std::string> fetch_c() { co_return "hello"; }
43+
io_task<int> fetch_a() { co_return io_result<int>{{}, 1}; }
44+
io_task<int> fetch_b() { co_return io_result<int>{{}, 2}; }
45+
io_task<std::string> fetch_c() { co_return io_result<std::string>{{}, "hello"}; }
4646
4747
task<> example()
4848
{
49-
auto [a, b, c] = co_await when_all(fetch_a(), fetch_b(), fetch_c());
50-
49+
auto [ec, a, b, c] = co_await when_all(fetch_a(), fetch_b(), fetch_c());
50+
51+
// ec == std::error_code{} (success)
5152
// a == 1
5253
// b == 2
5354
// c == "hello"
5455
}
5556
----
5657

57-
=== Result Tuple
58+
=== Result Type
5859

59-
`when_all` returns a tuple of results in the same order as the input tasks. Use structured bindings to unpack them.
60+
`when_all` returns `io_result<R1, ..., Rn>` where each `Ri` is the child's payload flattened: `io_result<T>` contributes `T`, `io_result<>` contributes `tuple<>`. Check `ec` first; values are only meaningful when `!ec`.
6061

61-
=== Void Tasks
62+
=== Void io_tasks
6263

63-
Tasks returning `void` contribute `std::monostate` to the result tuple, preserving the task-index-to-result-index mapping:
64+
`io_task<>` children contribute `tuple<>` to the result:
6465

6566
[source,cpp]
6667
----
67-
task<> void_task() { co_return; }
68-
task<int> int_task() { co_return 42; }
68+
io_task<> void_task() { co_return io_result<>{}; }
69+
io_task<int> int_task() { co_return io_result<int>{{}, 42}; }
6970
7071
task<> example()
7172
{
72-
auto [a, b, c] = co_await when_all(int_task(), void_task(), int_task());
73-
// a == 42 (int, index 0)
74-
// b == monostate (monostate, index 1)
75-
// c == 42 (int, index 2)
73+
auto [ec, a, b, c] = co_await when_all(int_task(), void_task(), int_task());
74+
// a == 42 (int)
75+
// b == tuple<> (from void io_task)
76+
// c == 42 (int)
7677
}
7778
----
7879

79-
If all tasks return `void`, `when_all` returns a tuple of `std::monostate`:
80+
When all children are `io_task<>`, just check `r.ec`:
8081

8182
[source,cpp]
8283
----
8384
task<> example()
8485
{
85-
auto [a, b] = co_await when_all(void_task_a(), void_task_b());
86-
// a and b are std::monostate
86+
auto r = co_await when_all(void_task_a(), void_task_b());
87+
if (r.ec)
88+
// handle error
8789
}
8890
----
8991

9092
=== Error Handling
9193

92-
If any task throws an exception:
94+
I/O errors are reported through the `ec` field of the `io_result`. When any child returns a non-zero `ec`:
95+
96+
1. Stop is requested for sibling tasks
97+
2. All tasks complete (or respond to stop)
98+
3. The first `ec` is propagated in the outer `io_result`
99+
100+
[source,cpp]
101+
----
102+
task<> example()
103+
{
104+
auto [ec, a, b] = co_await when_all(task_a(), task_b());
105+
if (ec)
106+
std::cerr << "Error: " << ec.message() << "\n";
107+
}
108+
----
93109

94-
1. The exception is captured
95-
2. Stop is requested for sibling tasks
96-
3. All tasks are allowed to complete (or respond to stop)
97-
4. The *first* exception is rethrown; later exceptions are discarded
110+
If a task throws an exception, it is captured and rethrown after all tasks complete. Exceptions take priority over `ec`.
98111

99112
[source,cpp]
100113
----
101-
task<int> might_fail(bool fail)
114+
io_task<int> might_throw(bool fail)
102115
{
103116
if (fail)
104117
throw std::runtime_error("failed");
105-
co_return 42;
118+
co_return io_result<int>{{}, 42};
106119
}
107120
108121
task<> example()
109122
{
110123
try
111124
{
112-
co_await when_all(might_fail(true), might_fail(false));
125+
co_await when_all(might_throw(true), might_throw(false));
113126
}
114127
catch (std::runtime_error const& e)
115128
{
@@ -124,23 +137,24 @@ When one task fails, `when_all` requests stop for its siblings. Well-behaved tas
124137

125138
[source,cpp]
126139
----
127-
task<> long_running()
140+
io_task<> long_running()
128141
{
129-
auto token = co_await get_stop_token();
130-
142+
auto token = co_await this_coro::stop_token;
143+
131144
for (int i = 0; i < 1000; ++i)
132145
{
133146
if (token.stop_requested())
134-
co_return; // Exit early when sibling fails
135-
147+
co_return io_result<>{}; // Exit early when sibling fails
148+
136149
co_await do_iteration();
137150
}
151+
co_return io_result<>{};
138152
}
139153
----
140154

141-
== when_any: First-to-Finish Wins
155+
== when_any: First-to-Succeed Wins
142156

143-
`when_any` launches multiple tasks concurrently and returns when the *first* one completes:
157+
`when_any` launches multiple `io_task` children concurrently and returns when the first one *succeeds* (`!ec`):
144158

145159
[source,cpp]
146160
----
@@ -149,17 +163,19 @@ task<> long_running()
149163
task<> example()
150164
{
151165
auto result = co_await when_any(
152-
fetch_int(), // task<int>
153-
fetch_string() // task<std::string>
166+
fetch_int(), // io_task<int>
167+
fetch_string() // io_task<std::string>
154168
);
155-
// result.index() indicates which task won (0 or 1)
156-
// result is std::variant<int, std::string>
169+
// result is std::variant<std::error_code, int, std::string>
170+
// index 0: all tasks failed (error_code)
171+
// index 1: fetch_int won
172+
// index 2: fetch_string won
157173
}
158174
----
159175

160-
The result is a variant with one alternative per input task, preserving positional correspondence. Use `.index()` to determine the winner. When a winner is determined, stop is requested for all siblings. All tasks complete before `when_any` returns.
176+
The result is a `variant` with `error_code` at index 0 (failure/no winner) and one alternative per input task at indices 1..N. Only tasks returning `!ec` can win; errors and exceptions do not count as winning. When a winner is found, stop is requested for all siblings. All tasks complete before `when_any` returns.
161177

162-
For detailed coverage including error handling, cancellation, and the vector overload, see Racing Tasks.
178+
For detailed coverage including error handling, cancellation, and the range overload, see Racing Tasks.
163179

164180
== Practical Patterns
165181

@@ -169,66 +185,68 @@ Fetch multiple resources simultaneously:
169185

170186
[source,cpp]
171187
----
172-
task<page_data> fetch_page_data(std::string url)
188+
io_task<page_data> fetch_page_data(std::string url)
173189
{
174-
auto [header, body, sidebar] = co_await when_all(
190+
auto [ec, header, body, sidebar] = co_await when_all(
175191
fetch_header(url),
176192
fetch_body(url),
177193
fetch_sidebar(url)
178194
);
179-
180-
co_return page_data{
195+
if (ec)
196+
co_return io_result<page_data>{ec, {}};
197+
198+
co_return io_result<page_data>{{}, {
181199
std::move(header),
182200
std::move(body),
183201
std::move(sidebar)
184-
};
202+
}};
185203
}
186204
----
187205

188206
=== Fan-Out/Fan-In
189207

190-
Process items in parallel, then combine results:
208+
Process items in parallel, then combine results using the range overload:
191209

192210
[source,cpp]
193211
----
194-
task<int> process_item(item const& i);
212+
io_task<int> process_item(item const& i);
195213
196214
task<int> process_all(std::vector<item> const& items)
197215
{
198-
std::vector<task<int>> tasks;
216+
std::vector<io_task<int>> tasks;
199217
for (auto const& item : items)
200218
tasks.push_back(process_item(item));
201-
202-
// This requires a range-based when_all (not yet available)
203-
// For now, use fixed-arity when_all
204-
219+
220+
auto [ec, results] = co_await when_all(std::move(tasks));
221+
if (ec)
222+
co_return 0;
223+
205224
int total = 0;
206-
// ... accumulate results
225+
for (auto v : results)
226+
total += v;
207227
co_return total;
208228
}
209229
----
210230

211-
=== Timeout with Fallback
231+
=== Timeout
212232

213-
Use `when_any` to implement timeout with fallback:
233+
The `timeout` combinator races an awaitable against a deadline:
214234

215235
[source,cpp]
216236
----
217-
task<Response> fetch_with_timeout(Request req)
218-
{
219-
auto result = co_await when_any(
220-
fetch_data(req),
221-
timeout_after<Response>(100ms)
222-
);
223-
224-
if (result.index() == 1)
225-
throw timeout_error{"Request timed out"};
237+
#include <boost/capy/timeout.hpp>
226238
227-
co_return std::get<0>(result);
239+
task<> example()
240+
{
241+
auto [ec, n] = co_await timeout(sock.read_some(buf), 50ms);
242+
if (ec == cond::timeout)
243+
{
244+
// deadline expired before read completed
245+
}
228246
}
229247
----
230248

231-
The `timeout_after` helper waits for the specified duration then throws. If `fetch_data` completes first, its result is returned. If the timer wins, the timeout exception propagates.
249+
`timeout` returns the same `io_result` type as the inner awaitable. On timeout, `ec` is set to `error::timeout` and payload values are default-initialized. Unlike `when_any`, exceptions from the inner awaitable are always propagated and never swallowed by the timer.
232250

233251
== Implementation Notes
234252

@@ -262,6 +280,9 @@ This design ensures proper context propagation to all children.
262280

263281
| `<boost/capy/when_any.hpp>`
264282
| First-completion racing with when_any
283+
284+
| `<boost/capy/timeout.hpp>`
285+
| Race an awaitable against a deadline
265286
|===
266287

267288
You have now learned how to compose tasks concurrently with `when_all` and `when_any`. In the next section, you will learn about frame allocators for customizing coroutine memory allocation.

doc/modules/ROOT/pages/7.examples/7b.producer-consumer.adoc

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,27 @@ int main()
3737
capy::async_event data_ready;
3838
int shared_value = 0;
3939
40-
auto producer = [&]() -> capy::task<> {
40+
auto producer = [&]() -> capy::io_task<> {
4141
std::cout << "Producer: preparing data...\n";
4242
shared_value = 42;
4343
std::cout << "Producer: data ready, signaling\n";
4444
data_ready.set();
45-
co_return;
45+
co_return capy::io_result<>{};
4646
};
4747
48-
auto consumer = [&]() -> capy::task<> {
48+
auto consumer = [&]() -> capy::io_task<> {
4949
std::cout << "Consumer: waiting for data...\n";
5050
auto [ec] = co_await data_ready.wait();
5151
(void)ec;
5252
std::cout << "Consumer: received value " << shared_value << "\n";
53-
co_return;
53+
co_return capy::io_result<>{};
5454
};
5555
5656
// Run both tasks concurrently using when_all, through a strand.
5757
// The strand serializes execution, ensuring thread-safe access
5858
// to the shared async_event and shared_value.
5959
auto run_both = [&]() -> capy::task<> {
60-
co_await capy::when_all(producer(), consumer());
60+
(void) co_await capy::when_all(producer(), consumer());
6161
};
6262
6363
capy::run_async(s, on_complete, on_error)(run_both());
@@ -99,12 +99,12 @@ capy::async_event data_ready;
9999

100100
[source,cpp]
101101
----
102-
auto producer = [&]() -> capy::task<> {
102+
auto producer = [&]() -> capy::io_task<> {
103103
std::cout << "Producer: preparing data...\n";
104104
shared_value = 42;
105105
std::cout << "Producer: data ready, signaling\n";
106106
data_ready.set();
107-
co_return;
107+
co_return capy::io_result<>{};
108108
};
109109
----
110110

@@ -114,12 +114,12 @@ The producer prepares data and signals completion by calling `set()`.
114114

115115
[source,cpp]
116116
----
117-
auto consumer = [&]() -> capy::task<> {
117+
auto consumer = [&]() -> capy::io_task<> {
118118
std::cout << "Consumer: waiting for data...\n";
119119
auto [ec] = co_await data_ready.wait();
120120
(void)ec;
121121
std::cout << "Consumer: received value " << shared_value << "\n";
122-
co_return;
122+
co_return capy::io_result<>{};
123123
};
124124
----
125125

@@ -133,7 +133,7 @@ The consumer waits until the event is set. The `co_await data_ready.wait()` susp
133133
// The strand serializes execution, ensuring thread-safe access
134134
// to the shared async_event and shared_value.
135135
auto run_both = [&]() -> capy::task<> {
136-
co_await capy::when_all(producer(), consumer());
136+
(void) co_await capy::when_all(producer(), consumer());
137137
};
138138
139139
capy::run_async(s, on_complete, on_error)(run_both());

0 commit comments

Comments
 (0)