Concurrent Composition
Headers: <boost/capy/when_all.hpp> and <boost/capy/when_any.hpp>. async_waker on this page is <boost/capy/ex/async_waker.hpp>.
Both combinators take task<results<…>> children. when_all returns a single results, and when_any returns a std::variant of the error code and the children’s payloads. If that type is new, read The results Type first — everything below is expressed in them.
Overview
Sequential execution—one task after another—is the default when using co_await:
capy::task<> sequential()
{
co_await task_a(); // Wait for A
co_await task_b(); // Then wait for B
co_await task_c(); // Then wait for C
}
For independent operations, concurrent execution is more efficient:
capy::task<> concurrent()
{
// Run A, B, C simultaneously
co_await capy::when_all(task_a(), task_b(), task_c());
}
when_all: Wait for All Tasks
when_all starts multiple task<results<…>> children concurrently and waits for all of them to complete. It returns task<results<R1, R2, …, Rn>>, a single ec plus the flattened payloads:
#include <boost/capy/when_all.hpp>
capy::task<capy::results<int>> fetch_a()
{
co_return capy::results<int>{std::error_code(), 1};
}
capy::task<capy::results<int>> fetch_b()
{
co_return capy::results<int>{std::error_code(), 2};
}
capy::task<capy::results<std::string>> fetch_c()
{
co_return capy::results<std::string>{std::error_code(), "hello"};
}
capy::task<> example()
{
auto [ec, a, b, c] = co_await capy::when_all(
fetch_a(), fetch_b(), fetch_c());
// ec == std::error_code{} (success)
// a == 1
// b == 2
// c == "hello"
}
Payload-free children
task<results<>> children contribute tuple<> to the result:
capy::task<capy::results<>> void_task() { co_return capy::results<>{}; }
capy::task<capy::results<int>> int_task()
{
co_return capy::results<int>{std::error_code(), 42};
}
capy::task<> example()
{
auto [ec, a, b, c] = co_await capy::when_all(
int_task(), void_task(), int_task());
// a == 42 (int)
// b == tuple<> (from task<results<>>)
// c == 42 (int)
}
When all children are task<results<>>, just check the error code:
capy::task<> example()
{
auto r = co_await capy::when_all(void_task_a(), void_task_b());
if (std::get<0>(r))
{
// handle error
}
}
Error Handling
I/O errors are reported through the error code that leads the results. When any child returns a non-zero error code:
-
Stop is requested for sibling tasks
-
All tasks complete (or respond to stop)
-
The first
ec(in completion order, not input order) is propagated in the outerresults
capy::task<> example()
{
auto [ec, a, b] = co_await capy::when_all(task_a(), task_b());
if (ec)
std::cerr << "Error: " << ec.message() << "\n";
}
If a task throws an exception, it is captured and rethrown after all tasks complete. Exceptions take priority over ec.
capy::task<capy::results<int>> might_throw(bool fail)
{
if (fail)
throw std::runtime_error("failed");
co_return capy::results<int>{std::error_code(), 42};
}
capy::task<> example()
{
try
{
co_await capy::when_all(might_throw(true), might_throw(false));
}
catch (std::runtime_error const& e)
{
// Catches the exception from the failing task
}
}
Stop Propagation
Well-behaved tasks should check their stop token and exit promptly:
capy::task<capy::results<>> long_running()
{
auto token = co_await capy::this_coro::stop_token;
for (int i = 0; i < 1000; ++i)
{
if (token.stop_requested())
co_return capy::results<>{}; // Exit early when sibling fails
co_await do_iteration();
}
co_return capy::results<>{};
}
when_any: First-to-Succeed Wins
when_any starts multiple task<results<…>> children concurrently and returns when the first one succeeds (!ec):
#include <boost/capy/when_any.hpp>
capy::task<> example()
{
auto result = co_await capy::when_any(
fetch_int(), // task<results<int>>
fetch_string() // task<results<std::string>>
);
// result is std::variant<std::error_code, int, std::string>
// index 0: all tasks failed (error_code)
// index 1: fetch_int won
// index 2: fetch_string won
}
The result is a variant with error_code at index 0 (failure/no winner) and one alternative per input task at indices 1..N. When a winner is found, stop is requested for all siblings. All tasks complete before when_any returns.
When every task fails, when_any reports a failure, but which one is unspecified. The result either carries an error_code at index 0 or rethrows one of the children’s exceptions. Unlike when_all, there is no priority between error codes and exceptions. In particular, there is no guarantee that it is the first or last to complete.
Errors Do Not Win (wait_for_one_success)
A child that returns a non-zero ec (or throws) does not win, and it does not cancel its siblings. when_any keeps waiting until some child succeeds or until every child has finished. Only when all children fail does the result settle at index 0, holding an error_code.
If you need "complete on the first child to finish, success or error," that behavior is opt-in — wrap the child as shown below.
Treating an Error as a Win
To make a child win on an error, wrap it so the error becomes a success before when_any sees it.
The first pattern translates a specific, benign error into success. Other errors propagate unchanged, so they still do not win:
// canceled is benign here: translate it to success so when_any picks this child.
capy::task<capy::results<>> wrapped()
{
auto [ec] = co_await inner();
if (ec == capy::cond::canceled)
co_return capy::results<>{}; // success: when_any sees a winner
co_return capy::results<>{ec}; // propagate other errors unchanged
}
The second pattern lifts the inner ec into the payload. The wrapper always succeeds, so it wins on its first completion, carrying the original error code to the caller:
// Always succeeds; the winner's payload carries the original ec.
capy::task<capy::results<std::error_code>> wrapped()
{
auto [ec] = co_await inner();
co_return capy::results<std::error_code>{std::error_code(), ec};
}
// when_any(wrapped(), ...) -> variant<error_code, std::error_code, ...>
// index 0: every child failed
// index i: child i won; std::get<i>(result) is its original ec
Practical Patterns
Parallel Fetch
Fetch multiple resources simultaneously:
capy::task<capy::results<page_data>> fetch_page_data(std::string url)
{
auto [ec, header, body, sidebar] = co_await capy::when_all(
fetch_header(url),
fetch_body(url),
fetch_sidebar(url)
);
if (ec)
co_return capy::results<page_data>{ec, {}};
co_return capy::results<page_data>{std::error_code(), {
std::move(header),
std::move(body),
std::move(sidebar)
}};
}
Fan-Out/Fan-In
Process items in parallel, then combine results using the range overload:
capy::task<capy::results<int>> process_item(item i);
capy::task<int> process_all(std::vector<item> const& items)
{
std::vector<capy::task<capy::results<int>>> tasks;
for (auto const& item : items)
tasks.push_back(process_item(item));
auto [ec, results] = co_await capy::when_all(std::move(tasks));
if (ec)
co_return 0;
int total = 0;
for (auto v : results)
total += v;
co_return total;
}
|
In the snippet above nothing dangles: |
Racing a Deadline
when_any is the tool for expressing a deadline race directly. Two waker waits, one completed by the operation itself, the other by whatever plays the clock, race as ordinary when_any children:
capy::task<capy::results<std::string>> await_fetch(fetch_channel& ch)
{
auto [ec] = co_await ch.fetch_ready.wait();
if (ec)
{
ch.cancelled.store(true);
co_return capy::results<std::string>{ec, {}};
}
co_return capy::results<std::string>{std::error_code(), std::move(ch.result)};
}
capy::task<capy::results<>> deadline(capy::async_waker& waker)
{
auto [ec] = co_await waker.wait();
co_return capy::results<>{ec};
}
auto race = [&]() -> capy::task<>
{
auto result = co_await capy::when_any(
await_fetch(fetch_ch),
deadline(deadline_waker));
winner = result.index();
if (result.index() == 1)
fetch_result = std::get<1>(std::move(result));
};
deadline_waker.wake() can come from any thread, a std::thread that sleeps for the deadline, hardware, or a completion callback from another library. See Timeout with Cancellation for the full example, including the fetch_channel type and both std::thread objects that play fetch worker and clock.
For timed I/O against a socket, prefer Corosio’s own timed operations: they schedule the deadline on the platform event loop instead of a dedicated thread.
Implementation Notes
Task Storage
when_all stores all tasks in its coroutine frame. Tasks are moved from the arguments, so the original task objects become empty after the call.