Launching Tasks

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

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 async_run 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/async_run.hpp>

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

The syntax async_run(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:

async_run(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:

async_run(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:

async_run(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:

async_run(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:

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

The Single-Expression Idiom

The async_run return value enforces a specific usage pattern:

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

// INCORRECT: Split across statements
auto runner = async_run(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, async_run uses a recycling allocator that caches deallocated frames. For custom allocation strategies:

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

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

When NOT to Use async_run

Use async_run 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 async_run when:

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

  • You need the result synchronously — async_run is asynchronous

Summary

Pattern Code

Fire and forget

async_run(ex)(task)

Success handler

async_run(ex)(task, handler)

Success + error handlers

async_run(ex)(task, on_success, on_error)

Custom allocator

async_run(ex, alloc)(task)

Next Steps