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:
-
Binds a task to a dispatcher (typically an executor)
-
Starts the task’s execution
-
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_awaitthe task directly -
You need the result synchronously —
async_runis asynchronous
Summary
| Pattern | Code |
|---|---|
Fire and forget |
|
Success handler |
|
Success + error handlers |
|
Custom allocator |
|
Next Steps
-
Executor Affinity — Control where coroutines execute
-
Frame Allocation — Optimize memory usage