tmc::spawn_group()#

spawn_group() provides an imperative interface for building a group of awaitables of the same type. Similar to spawn_many(), it allows you to customize execution behavior and wait for all awaitables to complete, but awaitables are collected incrementally rather than provided all at once. The wrapped awaitables will be executed concurrently.

Unlike fork_group, spawn_group is lazy - awaitables are not initiated until you co_await the group. This also means spawn_group is movable, so it can be returned from functions or passed around.

Key Differences#

spawn_group vs fork_group:

  • spawn_group is lazy - awaitables are collected but not initiated until co_await

  • spawn_group is movable - can be returned from functions or stored

  • spawn_group requires all awaitables to be the same type

  • spawn_group supports run_on(), resume_on(), and with_priority() customizations

spawn_group vs spawn_many:

  • spawn_group allows imperative construction - add awaitables one at a time

  • spawn_group can do HALO (via add_clang())

  • spawn_group can be reused (via reset())

Template Parameters#

spawn_group() has two template parameters:

  • MaxCount (default: 0): The maximum number of awaitables that may be added.

  • Awaitable (default: tmc::task<void>): The type of awaitables that will be added. All awaitables must be of this type.

Storage Strategies:

  • If MaxCount is non-zero, a fixed-size std::array<Awaitable, MaxCount> is used.

  • If MaxCount is 0, a std::vector<Awaitable> is used, allowing an unlimited number of awaitables.

Result Storage#

Results are returned the same way as spawn_many():

  • void if the awaitable result type is void

  • std::array<Result, MaxCount> for fixed-size groups

  • std::vector<Result> for dynamic-size groups

If the result type is not default-constructible, each value will be wrapped into std::optional<Result>.

Imperative Add Interface#

.add()#

Adds an awaitable to the group. The awaitable will be initiated when the group is co_awaited.

auto sg = tmc::spawn_group();
sg.add(task_void());
sg.add(another_task());
co_await std::move(sg);

.add_clang()#

Similar to add() but allows the child task’s allocation to be elided via HALO (Heap Allocation eLision Optimization) when compiled with Clang 20+. See HALO for details.

IMPORTANT: You must co_await the result of add_clang() immediately for HALO to be possible.

auto sg = tmc::spawn_group();
co_await sg.add_clang(task_void());
co_await sg.add_clang(another_task());
co_await std::move(sg);

WARNING: Do not use add_clang() in a loop, as Clang will try to reuse the same allocation for multiple active coroutines, causing crashes.

// WRONG - Will crash!
auto sg = tmc::spawn_group();
for (int i = 0; i < 2; i++) {
    co_await sg.add_clang(task(i));
}
co_await std::move(sg);

// The equivalent without the loop works fine.
auto sg = tmc::spawn_group();
co_await sg.add_clang(task(0));
co_await sg.add_clang(task(1));
co_await std::move(sg);

// Or you can use regular add() in a loop
// (but HALO will not be performed)
auto sg = tmc::spawn_group();
for (int i = 0; i < 2; i++) {
    sg.add(task(i));
}
co_await std::move(sg);

Awaitable Customizations#

The spawn_group awaitable supports these Awaitable Customizations: run_on(), resume_on(), with_priority(), co_await, fork()

Unlike fork_group, spawn_group requires you to customize the executor and priority for all tasks in the group at once:

auto sg = tmc::spawn_group();
sg.add(task1());
sg.add(task2());
co_await std::move(sg).run_on(tmc::cpu_executor()).with_priority(1);

// Or fork them
auto forked = std::move(sg).run_on(tmc::cpu_executor()).fork();
do_some_work();
co_await std::move(forked);

Movability#

Unlike fork_group, spawn_group is movable. This makes it useful for building task groups in helper functions:

auto build_tasks() -> tmc::aw_spawn_group<0, tmc::task<int>> {
    auto sg = tmc::spawn_group<0, tmc::task<int>>();
    sg.add(compute_task(1));
    sg.add(compute_task(2));
    return sg; // OK - spawn_group is movable
}

tmc::task<void> example() {
    auto sg = build_tasks();
    auto results = co_await std::move(sg);
}

API Reference#

template<size_t MaxCount = 0, typename Awaitable = tmc::task<void>>
aw_spawn_group<MaxCount, std::remove_cvref_t<Awaitable>> tmc::spawn_group()#

Constructs an empty spawn group with default template parameters.

template<size_t MaxCount = 0, typename Awaitable>
aw_spawn_group<MaxCount, std::remove_cvref_t<Awaitable>> tmc::spawn_group(Awaitable &&Aw)#

Constructs a spawn group with the first awaitable, deducing the awaitable type from the argument.

MaxCount is the maximum number of awaitables that will be added.

  • If MaxCount is non-zero, a fixed-size std::array will be allocated.

  • If MaxCount is zero, a std::vector will be used, allowing an unlimited number of awaitables to be added.

Awaitable is automatically deduced from the argument.

template<size_t MaxCount, typename Awaitable>
class aw_spawn_group : public tmc::detail::run_on_mixin<aw_spawn_group<MaxCount, Awaitable>>, public tmc::detail::resume_on_mixin<aw_spawn_group<MaxCount, Awaitable>>, public tmc::detail::with_priority_mixin<aw_spawn_group<MaxCount, Awaitable>>#

Similar to tmc::spawn_many(), but allows for imperative construction of the task group. Awaitables will be collected, but not initiated until you co_await this. Unlike tmc::fork_group, this type is movable, so it can be returned from functions or passed around.

Awaitable is the type of awaitable that will be added to the group. All awaitables added to the group must be of the same type.

MaxCount is the maximum number of awaitables that may be added.

  • If MaxCount is non-zero, a fixed-size std::array will be used. You can add up to this many awaitables to the group. If less than MaxCount awaitables are added, the remaining results in the result array will be default-initialized.

  • If MaxCount is zero, an unlimited number of awaitables can be added. The results will be returned in a right-sized std::vector.

Public Functions

inline aw_spawn_group()#

Constructs an empty spawn group. It is recommended to use tmc::spawn_group() instead of this constructor.

inline aw_spawn_group(Awaitable &&Aw)#

Constructs an empty spawn group. It is recommended to use tmc::spawn_group(Awaitable&& Aw) instead of this constructor.

inline void add(Awaitable &&Aw)#

Adds an awaitable to the group. The awaitable will be initiated when the group is co_awaited.

inline aw_spawn_group_add_clang add_clang(Awaitable &&Aw)#

Similar to add() but allows the child task’s allocation to be elided by combining it into the parent’s allocation (HALO). This works by using specific attributes that are only available on Clang 20+. You can safely call this function on other compilers, but no HALO-specific optimizations will be applied.

WARNING: Don’t allow coroutines passed into this to cross a loop boundary, or Clang will try to reuse the same allocation for multiple active coroutines.

// the following usage will make your program CRASH!
auto sg = tmc::spawn_group();
for (int i = 0; i < 2; i++) {
  co_await sg.add_clang(task(i));
}
co_await std::move(sg);

IMPORTANT: This returns a dummy awaitable. For HALO to work, you should not store the dummy awaitable. Instead, co_await this expression immediately. Proper usage:

// note that this is the same as the prior example
// but without the loop, it works fine
auto sg = tmc::spawn_group();
co_await sg.add_clang(task(0));
co_await sg.add_clang(task(1));
co_await std::move(sg);

inline aw_spawn_many_impl<Result, MaxCount, false, false> operator co_await() && noexcept#

Initiates all of the wrapped awaitables and waits for them to complete.

inline aw_spawn_many_fork<Result, MaxCount, false> fork() && noexcept#

Initiates all of the wrapped awaitables, without suspending the current coroutine. You must join them by awaiting the returned awaitable before it goes out of scope.

inline void reset() noexcept#

After you co_await this group, you may call reset() to make it usable again. This allows you to accumulate and co_await another group of awaitables.

inline size_t capacity() noexcept#

Returns the maximum capacity of the spawn_group as determined by the MaxCount parameter.

If the capacity is unlimited, this will return std::numeric_limits<size_t>::max(), i.e. static_cast<size_t>(-1).

inline size_t size() noexcept#

Returns the number of awaitables actually dispatched to the spawn_group. This value will be reset to 0 when reset() is called.

class aw_spawn_group_add_clang : private tmc::detail::AwaitTagNoGroupAsIs#

This is a dummy awaitable. Don’t store this in a variable. For HALO to work, you must co_await sg.add_clang() immediately.

Public Functions

inline bool await_ready() const noexcept#

Never suspends.

inline void await_suspend(std::coroutine_handle<>) noexcept#

Does nothing.

inline void await_resume() noexcept#

Does nothing.