Coroutines / tmc::task#
TMC’s coroutine type is tmc::task<Result>. It is a lazy/cold coroutine, and will not start executing until it is co_await ed, submitted to an executor, or manually resumed.
It is a work item, so it is compatible with the tmc::post*() family of functions.
It is an awaitable, so it is compatible with the tmc::spawn*() family of functions.
Task is a linear type. It must be passed around by move, and then ultimately awaited exactly once.
It cannot be copied.
It must be moved into any operation that uses it. This includes:
co_awaitpost*()spawn*()
A moved-from task must not be used afterward.
It will destroy its associated coroutine frame/promise after it completes.
The result that it produces will be available in the awaiting coroutine’s scope.
It cannot be co_awaited again to produce the same result again.
Rules For Safe Usage of Coroutines#
Lambda Captures#
If you use a lambda task, its capture list must be completely empty ([]). This is because the lambda will return immediately and may be destroyed, but the
task’s lifetime will be longer and it cannot have a dangling reference to the lambda object.
int a;
auto r = co_await tmc::spawn_many(
std::ranges::iota_view(0,5) |
std::ranges::views::transform(
// This is undefined behavior
[&a](int i) -> tmc::task<int> { co_return a + i; }
)
);
If you need to capture a value or reference into a coroutine, you must pass it as a parameter.
You can use a lambda wrapper to simplify this. Note that the lambda wrapper is not a coroutine
(it uses return rather than co_return) - it just invokes the wrapped coroutine function
and returns the coroutine object.
int a;
auto r = co_await tmc::spawn_many(
std::ranges::iota_view(0, 5) |
std::ranges::views::transform(
// This is safe, by passing the capture through to a parameter
[&a](int i) -> tmc::task<int> {
return [](int& x, int y) -> tmc::task<int> { co_return x + y; }(a, i);
}
)
);
Since coroutines cannot capture, it may be cleaner to adopt the guideline to always use named coroutines:
tmc::task<int> adder(int& x, int y) {
co_return x + y;
}
int a;
auto r = co_await tmc::spawn_many(
std::ranges::iota_view(0,5) |
std::ranges::views::transform(
// This is safe, by passing the capture through to a parameter
[&a](int i) -> tmc::task<int>{ return adder(a,i); }
)
);
If you need to capture this, then consider making the coroutine a member function.
Lifetimes of Externally Referenced Data#
If a coroutine references some external data, you must ensure that the external data remains in scope as long as the coroutine is executing. This is most relevant when coroutines reference data that is present in their caller’s scope.
This is true for both synchronous / blocking APIs:
tmc::task<void> handler(int& v) {
// do something with v
}
void func() {
int a;
// This is safe. It waits for the handler to complete, so `a` stays in scope.
tmc::post_waitable(tmc::cpu_executor(), handler(a)).wait();
// This is not safe. Handler may run at some point in the future, after this function returns.
tmc::post(tmc::cpu_executor(), handler(a));
}
And for asynchronous / suspending APIs:
tmc::task<void> handler(int& v) {
// do something with v
}
tmc::task<void> coro() {
int a;
// This is safe. It waits for the handler to complete, so `a` stays in scope.
co_await handler(a);
// This is not safe. Handler may run at some point in the future, after this coroutine ends.
tmc::spawn(handler(a)).detach();
}
Forking APIs are safe as long as they are awaited before the referenced object goes out of scope:
tmc::task<void> handler(int& v) {
// do something with v
}
tmc::task<void> coro() {
int a;
// This is safe, as long as we await it before this goes out of scope.
auto tFork = tmc::spawn(handler(a)).fork();
// ... do other stuff while the forked task runs in parallel
// Ensure the forked task has finished.
co_await std::move(tFork);
}
API Reference#
-
template<typename Result>
struct task# The main coroutine type used by TooManyCooks.
taskis a lazy / cold coroutine and will not begin running immediately. To start running atask, you can:Use
co_awaitdirectly on the task to run it and await the results.Call
tmc::spawn()to create a task wrapper that can be configured beforeco_awaiting the results.Call
tmc::spawn_many()to submit and await multiple tasks at once. This task group can be configured beforeco_awaiting the results.Call
tmc::post()/tmc::post_waitable()to submit this task for execution to an async executor from external (non-async) calling code.Public Functions
-
inline aw_task<task<Result>, Result> operator co_await() && noexcept#
Suspend the outer coroutine and run this task directly. The intermediate awaitable type
aw_taskcannot be used directly; the return type of theco_awaitexpression will beResultorvoid.
-
inline task &resume_on(tmc::ex_any *Executor) & noexcept#
When this task completes, the awaiting coroutine will be resumed on the provided executor.
-
template<typename Exec>
inline task &resume_on(Exec &&Executor) & noexcept# When this task completes, the awaiting coroutine will be resumed on the provided executor.
-
template<typename Exec>
inline task &resume_on(Exec *Executor) & noexcept# When this task completes, the awaiting coroutine will be resumed on the provided executor.
-
inline task &&resume_on(tmc::ex_any *Executor) && noexcept#
When this task completes, the awaiting coroutine will be resumed on the provided executor.
-
template<typename Exec>
inline task &&resume_on(Exec &&Executor) && noexcept# When this task completes, the awaiting coroutine will be resumed on the provided executor.
-
template<typename Exec>
inline task &&resume_on(Exec *Executor) && noexcept# When this task completes, the awaiting coroutine will be resumed on the provided executor.
-
inline task(std::coroutine_handle<promise_type> &&Other) noexcept#
Tasks are move-only.
-
inline ~task()#
When this task is destroyed, it should already have been deinitialized. Either because it was moved-from, or because the coroutine completed.
-
inline operator std::coroutine_handle<>() && noexcept#
Conversion to a std::coroutine_handle<> is move-only.
-
inline operator std::coroutine_handle<promise_type>() && noexcept#
Conversion to a std::coroutine_handle<> is move-only.
-
inline aw_task<task<Result>, Result> operator co_await() && noexcept#