Launching Tasks

This page explains how to start lazy tasks for execution using run_async.

Code snippets assume using namespace boost::capy; is in effect.

Why Tasks Need a Driver

Tasks are lazy. They remain suspended until something starts them. Within a coroutine, co_await serves this purpose. But at the program’s entry point, you need a way to kick off the first coroutine.

The run_async function provides this capability. It:

  1. Binds a task to a dispatcher (typically an executor)

  2. Starts the task’s execution

  3. Optionally delivers the result to a completion handler

Basic Usage

#include <boost/capy/ex/run_async.hpp>

void start(executor ex)
{
    run_async(ex)(compute());
}

The syntax run_async(ex)(task) creates a runner bound to the executor, then immediately launches the task. The task begins executing when the executor schedules it.

Fire and Forget

The simplest pattern discards the result:

run_async(ex)(compute());

If the task throws an exception, it propagates to the executor’s error handling (typically rethrown from run()). This pattern is appropriate for top-level tasks where errors should terminate the program.

Handling Results

To receive the task’s result, provide a completion handler:

run_async(ex)(compute(), [](int result) {
    std::cout << "Got: " << result << "\n";
});

The handler is called when the task completes successfully. For task<void>, the handler takes no arguments:

run_async(ex)(do_work(), []() {
    std::cout << "Work complete\n";
});

Handling Errors

To handle both success and failure, provide a handler that also accepts std::exception_ptr:

run_async(ex)(compute(), overloaded{
    [](int result) {
        std::cout << "Success: " << result << "\n";
    },
    [](std::exception_ptr ep) {
        try {
            if (ep) std::rethrow_exception(ep);
        } catch (std::exception const& e) {
            std::cerr << "Error: " << e.what() << "\n";
        }
    }
});

Alternatively, use separate handlers for success and error:

run_async(ex)(compute(),
    [](int result) { std::cout << result << "\n"; },
    [](std::exception_ptr ep) { /* handle error */ }
);

The Single-Expression Idiom

The run_async return value enforces a specific usage pattern:

// CORRECT: Single expression
run_async(ex)(make_task());

// INCORRECT: Split across statements
auto runner = run_async(ex);  // Sets thread-local state
// ... other code may interfere ...
runner(make_task());          // Won't compile (deleted move)

This design ensures the frame allocator is active when your task is created, enabling frame recycling optimization.

Custom Frame Allocators

By default, run_async uses a recycling allocator that caches deallocated frames. For custom allocation strategies:

my_pool_allocator alloc{pool};
run_async(ex, alloc)(my_task());

The allocator is used for all coroutine frames in the launched call tree.

When NOT to Use run_async

Use run_async when:

  • You need to start a coroutine from non-coroutine code

  • You want fire-and-forget semantics

  • You need to receive the result via callback

Do NOT use run_async when:

  • You are already inside a coroutine — just co_await the task directly

  • You need the result synchronously — run_async is asynchronous

Summary

Pattern Code

Fire and forget

run_async(ex)(task)

Success handler

run_async(ex)(task, handler)

Success + error handlers

run_async(ex)(task, on_success, on_error)

Custom allocator

run_async(ex, alloc)(task)

Next Steps