Tasks
This page explains how to write coroutine functions using task<T>.
Code snippets assume using namespace boost::capy; is in effect.
|
What is a Task?
A task<T> represents an asynchronous operation that will produce a value
of type T. Tasks are lazy: they do not begin execution when created.
A task remains suspended until it is either awaited by another coroutine
or launched explicitly with async_run.
This laziness enables structured composition. When you write:
task<void> parent()
{
co_await child(); // child runs here, not when created
}
The child coroutine starts exactly when the parent awaits it, making the control flow predictable.
Creating Tasks
Write a coroutine function by using co_return or co_await:
task<int> compute()
{
co_return 42;
}
The function compute() returns immediately with a suspended coroutine.
No code inside the function body executes until the task is started.
Awaiting Tasks
Tasks can await other tasks using co_await. The calling coroutine suspends
until the awaited task completes:
task<int> step_one()
{
co_return 10;
}
task<int> step_two(int x)
{
co_return x * 2;
}
task<int> pipeline()
{
int a = co_await step_one();
int b = co_await step_two(a);
co_return a + b; // 10 + 20 = 30
}
Each co_await suspends the current coroutine, starts the child task, and
resumes when the child completes. The child’s return value becomes the result
of the co_await expression.
Exception Handling
Exceptions thrown within a task are captured and stored. When the task is awaited, the exception is rethrown in the awaiting coroutine:
task<int> might_fail()
{
throw std::runtime_error("oops");
co_return 0; // never reached
}
task<void> caller()
{
try {
int x = co_await might_fail();
} catch (std::exception const& e) {
std::cerr << "Caught: " << e.what() << "\n";
}
}
This enables natural exception handling across coroutine boundaries.
Move-Only Semantics
Tasks are move-only. You cannot copy a task:
task<int> t = compute();
task<int> t2 = t; // ERROR: deleted copy constructor
task<int> t3 = std::move(t); // OK: move is allowed
This reflects the fact that a coroutine has unique state that cannot be duplicated.
Releasing the Handle
In advanced scenarios, you may need direct access to the coroutine handle.
The release() method transfers ownership:
task<int> t = compute();
auto handle = t.release(); // t no longer owns the coroutine
// ... use handle directly ...
handle.destroy(); // caller is responsible for cleanup
After calling release(), the task is empty and must not be awaited.
|
When NOT to Use Tasks
Tasks are appropriate when:
-
The operation may suspend (performs I/O, awaits other tasks)
-
You want structured composition with parent/child relationships
-
You need lazy evaluation
Tasks are NOT appropriate when:
-
The operation is purely synchronous — just use a regular function
-
You need parallel execution — tasks are sequential; use parallel composition
-
You need to detach and forget — tasks must be awaited or explicitly launched
Summary
| Feature | Description |
|---|---|
Lazy execution |
Tasks do not start until awaited or launched |
Move-only |
Cannot copy, can move |
Exception propagation |
Exceptions rethrow at the await point |
Structured |
Parent awaits child, control flow is predictable |
Next Steps
Now that you understand tasks, learn how to run them:
-
Launching Tasks — Start tasks with
async_run -
Executor Affinity — Control where tasks execute