Concurrent Composition

This section explains how to run multiple tasks concurrently using when_all and when_any.

Prerequisites

Overview

Sequential execution—one task after another—is the default when using co_await:

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:

task<> concurrent()
{
    // Run A, B, C simultaneously
    co_await when_all(task_a(), task_b(), task_c());
}

when_all: Wait for All Tasks

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:

#include <boost/capy/when_all.hpp>

io_task<int> fetch_a() { co_return io_result<int>{{}, 1}; }
io_task<int> fetch_b() { co_return io_result<int>{{}, 2}; }
io_task<std::string> fetch_c() { co_return io_result<std::string>{{}, "hello"}; }

task<> example()
{
    auto [ec, a, b, c] = co_await when_all(fetch_a(), fetch_b(), fetch_c());

    // ec == std::error_code{} (success)
    // a == 1
    // b == 2
    // c == "hello"
}

Result Type

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.

Void io_tasks

io_task<> children contribute tuple<> to the result:

io_task<> void_task() { co_return io_result<>{}; }
io_task<int> int_task() { co_return io_result<int>{{}, 42}; }

task<> example()
{
    auto [ec, a, b, c] = co_await when_all(int_task(), void_task(), int_task());
    // a == 42       (int)
    // b == tuple<>  (from void io_task)
    // c == 42       (int)
}

When all children are io_task<>, just check r.ec:

task<> example()
{
    auto r = co_await when_all(void_task_a(), void_task_b());
    if (r.ec)
        // handle error
}

Error Handling

I/O errors are reported through the ec field of the io_result. When any child returns a non-zero ec:

  1. Stop is requested for sibling tasks

  2. All tasks complete (or respond to stop)

  3. The first ec is propagated in the outer io_result

task<> example()
{
    auto [ec, a, b] = co_await 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.

io_task<int> might_throw(bool fail)
{
    if (fail)
        throw std::runtime_error("failed");
    co_return io_result<int>{{}, 42};
}

task<> example()
{
    try
    {
        co_await when_all(might_throw(true), might_throw(false));
    }
    catch (std::runtime_error const& e)
    {
        // Catches the exception from the failing task
    }
}

Stop Propagation

When one task fails, when_all requests stop for its siblings. Well-behaved tasks should check their stop token and exit promptly:

io_task<> long_running()
{
    auto token = co_await this_coro::stop_token;

    for (int i = 0; i < 1000; ++i)
    {
        if (token.stop_requested())
            co_return io_result<>{};  // Exit early when sibling fails

        co_await do_iteration();
    }
    co_return io_result<>{};
}

when_any: First-to-Succeed Wins

when_any launches multiple io_task children concurrently and returns when the first one succeeds (!ec):

#include <boost/capy/when_any.hpp>

task<> example()
{
    auto result = co_await when_any(
        fetch_int(),     // io_task<int>
        fetch_string()   // io_task<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. 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.

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.
io_task<> wrapped()
{
    auto [ec] = co_await inner();
    if (ec == cond::canceled)
        co_return io_result<>{};   // success: when_any sees a winner
    co_return io_result<>{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.
io_task<std::error_code> wrapped()
{
    auto [ec] = co_await inner();
    co_return io_result<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:

io_task<page_data> fetch_page_data(std::string url)
{
    auto [ec, header, body, sidebar] = co_await when_all(
        fetch_header(url),
        fetch_body(url),
        fetch_sidebar(url)
    );
    if (ec)
        co_return io_result<page_data>{ec, {}};

    co_return io_result<page_data>{{}, {
        std::move(header),
        std::move(body),
        std::move(sidebar)
    }};
}

Fan-Out/Fan-In

Process items in parallel, then combine results using the range overload:

io_task<int> process_item(item const& i);

task<int> process_all(std::vector<item> const& items)
{
    std::vector<io_task<int>> tasks;
    for (auto const& item : items)
        tasks.push_back(process_item(item));

    auto [ec, results] = co_await when_all(std::move(tasks));
    if (ec)
        co_return 0;

    int total = 0;
    for (auto v : results)
        total += v;
    co_return total;
}

Timeout

The timeout combinator races an awaitable against a deadline:

#include <boost/capy/timeout.hpp>

task<> example()
{
    auto [ec, n] = co_await timeout(sock.read_some(buf), 50ms);
    if (ec == cond::timeout)
    {
        // deadline expired before read completed
    }
}

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.

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.

Completion Tracking

A shared atomic counter tracks how many tasks remain. Each task completion decrements the counter. When it reaches zero, the parent coroutine is resumed.

Runner Coroutines

Each child task is wrapped in a "runner" coroutine that:

  1. Receives context (executor, stop token) from when_all

  2. Awaits the child task

  3. Stores the result in shared state

  4. Signals completion

This design ensures proper context propagation to all children.

Reference

Header Description

<boost/capy/when_all.hpp>

Concurrent composition with when_all

<boost/capy/when_any.hpp>

First-completion racing with when_any

<boost/capy/timeout.hpp>

Race an awaitable against a deadline

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.