tmc::fork_group()#

fork_group() provides an imperative interface for forking multiple awaitables. Unlike spawn_many() or spawn_tuple(), which require all awaitables to be known upfront, fork_group() allows you to incrementally fork awaitables and join them all at a later time.

Each awaitable is initiated immediately (concurrently) when fork() is called. The fork_group must be awaited to join all forked tasks before it goes out of scope.

Template Parameters#

fork_group() has two template parameters that determine its storage strategy:

  • MaxCount (default: 0): The maximum number of awaitables that will be forked.

  • Result (default: void): The result type of the awaitables that will be forked.

Storage Strategies:

  • If Result is void, then MaxCount must be 0. No storage is needed, so an unlimited number of void-returning awaitables can be forked with no space overhead.

  • If Result is non-void and MaxCount is non-zero, a fixed-size std::array<Result, MaxCount> is used. If fewer than MaxCount awaitables are forked, the remaining results are default-initialized.

  • If Result is non-void and MaxCount is 0, you must pass a RuntimeMaxCount parameter at construction. A std::vector<Result> of this size is pre-allocated.

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

Imperative Fork Interface#

.fork()#

Initiates an awaitable immediately on the specified executor and priority. The awaitable can be any awaitable type as long as its result type matches the fork_group’s Result type.

auto fg = tmc::fork_group();
fg.fork(task_void());
fg.fork(some_awaitable());
co_await std::move(fg);

.fork_clang()#

Similar to fork() but allows the forked 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 fork_clang() immediately for HALO to be possible.

auto fg = tmc::fork_group();
co_await fg.fork_clang(task_void());
co_await fg.fork_clang(another_task());
co_await std::move(fg);

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

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

// The equivalent without the loop works fine.
auto fg = tmc::fork_group();
co_await fg.fork_clang(task(0));
co_await fg.fork_clang(task(1));
co_await std::move(fg);

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

Awaitable Customizations#

The fork_group awaitable supports this Awaitable Customization: resume_on()

Note that fork_group does not support run_on() or with_priority() on the group itself. Instead, you specify the executor and priority for each individual forked task via the fork() method parameters.

API Reference#

template<size_t MaxCount = 0, typename Result = void>
aw_fork_group<MaxCount, Result> tmc::fork_group()#

Constructs an empty fork group with default template parameters.

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

  • If Result is void (default), then MaxCount must be 0. This allows you to fork an unlimited number of void-returning awaitables.

  • If Result is non-void and MaxCount is non-zero, a fixed-size result array is used. If less than MaxCount awaitables are forked, the remaining results will be default-initialized.

  • If Result is non-void and MaxCount is 0, you must use the fork_group<Result>(RuntimeMaxCount) overload instead.

Result is the result type of the awaitables that will be forked.

template<size_t MaxCount = 0, typename Awaitable, typename Result = tmc::detail::awaitable_result_t<Awaitable>>
aw_fork_group<MaxCount, Result> tmc::fork_group(Awaitable &&Aw)#

Constructs a fork group with the first awaitable, deducing the result type from the awaitable’s return type.

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

  • If Result is void, then MaxCount must be 0. This allows you to fork an unlimited number of void-returning awaitables.

  • If Result is non-void and MaxCount is non-zero, a fixed-size result array is used. If less than MaxCount awaitables are forked, the remaining results will be default-initialized.

  • If Result is non-void and MaxCount is 0, you must use the fork_group<Result>(RuntimeMaxCount) overload instead.

template<typename Result>
aw_fork_group<0, Result> tmc::fork_group(size_t RuntimeMaxCount)#

Constructs an empty fork group with runtime-specified maximum size.

Result is the result type of the awaitables that will be forked. Result must be non-void.

RuntimeMaxCount is the maximum number of awaitables that may be forked. A std::vector of this size will be pre-allocated to store the results. If less than RuntimeMaxCount awaitables are forked, the remaining results will be default-initialized.

template<typename Awaitable, typename Result = tmc::detail::awaitable_result_t<Awaitable>>
aw_fork_group<0, Result> tmc::fork_group(size_t RuntimeMaxCount, Awaitable &&Aw)#

Constructs a fork group with runtime-specified maximum size and the first awaitable, deducing the result type from the awaitable. The deduced result type must be non-void.

RuntimeMaxCount is the maximum number of awaitables that may be forked. A std::vector of this size will be pre-allocated to store the results. If less than RuntimeMaxCount awaitables are forked, the remaining results will be default-initialized.

template<size_t MaxCount, typename Result>
class aw_fork_group : public tmc::detail::resume_on_mixin<aw_fork_group<MaxCount, Result>>#

A container for imperatively forking awaitables, initiating each awaitable immediately, and joining them all at a later time.

Result is the result type of the awaitables that will be forked. Different types of awaitables may be forked with a single fork_group, as long as they all return the same Result type.

MaxCount determines the result storage strategy:

  • If Result is void, then MaxCount must be 0. No storage is needed, so an unlimited number of awaitables can be forked.

  • If Result is non-void and MaxCount is non-zero, a fixed-size std::array<Result, MaxCount> is used. You can fork up to MaxCount awaitables. If less than MaxCount awaitables are forked, the remaining results in the result array are default-initialized.

  • If Result is non-void and MaxCount is zero, a std::vector<Result> is used. The size of this vector is fixed at construction time by passing the RuntimeMaxCount parameter. You can fork up to RuntimeMaxCount awaitables. If less than RuntimeMaxCount awaitables are forked, the remaining results in the result vector are default-initialized.

Public Functions

inline aw_fork_group()#

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

inline aw_fork_group(size_t RuntimeMaxCount)#

Constructs an empty fork group with runtime size. It is recommended to use tmc::fork_group(RuntimeMaxCount) instead of this constructor.

template<typename Awaitable>
inline aw_fork_group(Awaitable &&Aw)#

Constructs a fork group with the first awaitable. It is recommended to use tmc::fork_group(Awaitable) instead of this constructor.

template<typename Awaitable>
inline aw_fork_group(size_t RuntimeMaxCount, Awaitable &&Aw)#

Constructs a fork group with runtime size and the first awaitable. It is recommended to use tmc::fork_group(RuntimeMaxCount, Awaitable) instead of this constructor.

template<typename Awaitable, typename Exec = tmc::ex_any*>
inline void fork(Awaitable &&Aw, Exec &&Executor = tmc::current_executor(), size_t Priority = tmc::current_priority())#

Initiates Awaitable immediately on the specified executor and priority. Awaitable can be any awaitable type as long as its result type matches the fork_group’s Result type.

Executor defaults to the current executor. Priority defaults to the current priority.

template<typename Awaitable, typename Exec = tmc::ex_any*>
inline aw_fork_group_fork_clang fork_clang(Awaitable &&Aw, Exec &&Executor = tmc::current_executor(), size_t Priority = tmc::current_priority())#

Similar to fork() but allows the forked 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 fg = tmc::fork_group();
for (int i = 0; i < 2; i++) {
  co_await fg.fork_clang(task(i));
}
co_await std::move(fg);

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 fg = tmc::fork_group();
co_await fg.fork_clang(task(0));
co_await fg.fork_clang(task(1));
co_await std::move(fg);

inline bool await_ready() const noexcept#

Always suspends.

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

Suspends the outer coroutine and waits for the forked awaitables to complete.

inline std::add_rvalue_reference_t<ResultArray> await_resume() noexcept#

Returns the result array.

inline void await_resume() noexcept

Does nothing.

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.

For fixed-size fork_groups (MaxCount != 0), this resets the task count and done count, allowing the same result storage to be reused.

For runtime-sized fork_groups (MaxCount == 0 with non-void Result), this overload cannot be used. Instead, use reset(RuntimeMaxCount) to specify a new capacity.

inline void reset(size_t RuntimeMaxCount) noexcept#

After you co_await this group, you may call reset(RuntimeMaxCount) to make it usable again with a new capacity.

Only available for runtime-sized fork_groups (MaxCount == 0 with non-void Result). The result vector is resized to the new RuntimeMaxCount.

inline size_t capacity() noexcept#

Returns the maximum capacity of the fork_group as determined by the MaxCount or RuntimeMaxCount parameters. Note that this function is only guaranteed to return a valid value before this is co_await ed.

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 fork_group. This value will be reset to 0 when reset() is called.

class aw_fork_group_fork_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 fg.fork_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.