Awaitables / tmc::spawn()#

From within a tmc::task, you can co_await any awaitable (including other tmc::task) directly.

Awaitable Customizers#

Customize the dispatch behavior of a single awaitable:

tmc::spawn()

Customize a single awaitable

tmc::spawn_func()

Convert a single functor into an awaitable

Awaitable Grouping Functions#

Combine and customize multiple awaitables:

Homogeneous (awaitables must be all of the same type, and return the same type)#

tmc::spawn_many()

Execute multiple awaitables concurrently & get results in an array or vector

tmc::spawn_func_many()

Execute multiple functors concurrently & get results in an array or vector

tmc::spawn_group()

Imperatively build a group of same-typed awaitables (like spawn_many)

Awaitable Heterogeneous / Results Homogeneous (awaitables may be of different types, but must all return the same type)#

tmc::fork_group()

Imperatively fork awaitables of any type (sharing one result type) and join them later

tmc::mux_many

Multiplex awaitables, consuming each result as it becomes ready. Optionally re-arm completed slots.

Fully Heterogeneous (awaitables may be of different types, and may return different types)#

tmc::spawn_tuple()

Execute multiple awaitables concurrently & get results in a tuple

tmc::mux_tuple

Multiplex awaitables, consuming each result as it becomes ready. Optionally re-arm completed slots.

tmc::select()

Await several awaitables and take the result of the first to complete, cancelling the rest

Group Composition#

Since the output of the functions above is an awaitable, they can be composed by passing them into each other.

// This example constructs several different awaitable groups using different methods.
// At the end, they are all composed into a spawn_tuple.

tmc::task<Model> load_model(std::string filename); // a coroutine
TextureLoadAwaitable load_texture(std::string filename); // a non-coroutine awaitable
Shader compile_shader(ShaderSource src); // a regular function

struct EntityInfo {
  std::string modelFile;
  std::vector<std::string> textureFiles;
  std::vector<ShaderSource> shaderSources;

  // This function is not a coroutine - it just returns the awaitable group.
  // It could be made into a coroutine that awaits the awaitable group and returns
  // the result, and the caller's syntax would be the same either way.
  auto load() {
    auto textureTasks = tmc::spawn_many(
      textureFiles | std::ranges::views::transform(load_texture)
    );

    auto shaderTasks = tmc::spawn_many(
      shaderSources | std::ranges::views::transform([](ShaderSource src) {
        // make the regular function into an awaitable
        return tmc::spawn_func(compile_shader, src);
      })
    );

    return tmc::spawn_tuple(
      load_model(modelFile),
      std::move(textureTasks),
      std::move(shaderTasks)
    );
  }
};

tmc::task<void> entity_loader(EntityInfo& e) {
  // Run all of the subtasks (and sub-subtasks) concurrently and wait for the results.
  // The return type is `std::tuple<Model, std::vector<Texture>, std::vector<Shader>>`
  auto [model, textures, shaders] = co_await e.load();
}

Rvalue-Only Awaitables#

Most TMC awaitables (including tmc::task) are single-use / “rvalue-only awaitables” / linear types. They must be used as temporaries, or passed as rvalue references, and then ultimately consumed.

  • They cannot be copied.

  • They must be moved into a consuming operation exactly once before they go out of scope. The consuming operations are co_await, spawn_*(), fork(), or detach().

  • Moved-from awaitables must not be used afterward.

  • The result of a co_await expression must be stored in a value (not a reference).

Note that the awaitables produced by spawn_*() and fork() are also subject to these rules. So if you move a task into spawn(), the task is considered consumed, but spawn() returns a new awaitable object, which must itself be consumed.

The purpose of these rules is to help you prevent leaks / use-after-free issues. Where possible, these rules are enforced at compile time. Additionally, in Debug builds, there are runtime asserts that verify that you have not violated the above preconditions. In Release builds, these runtime checks are inactive for performance reasons.

The easiest way to use rvalue-only awaitables is to use them as temporaries in immediately-awaited expressions:

// temporary rvalue
co_await expr();

// explicit rvalue cast required
auto t = expr();
co_await std::move(t);

// wrappers have the same rules - temporary rvalue
co_await spawn_tuple(expr());

// explicit rvalue cast required
auto t = expr();
co_await tmc::spawn_tuple(std::move(t));

Lvalue-Only Awaitables#

A small number of TMC awaitables are “lvalue-only awaitables”. For example, tmc::mux_many and tmc::mux_tuple must be awaited multiple times to produce a sequence of values. These types cannot be awaited as rvalues or xvalues - you must first assign them to a named variable before awaiting them.

// temporary rvalue not allowed, an lvalue must be created
auto mux = tmc::mux_tuple(expr0(), expr1());

// await it repeatedly to consume each result as it becomes ready
for (size_t i = co_await mux; i != mux.end(); i = co_await mux) {
  // handle the ready result at index i ...
}

Executor and Priority Affinity#

By default, when an awaitable completes, it will restore the awaiting coroutine to its original executor and original priority. This behavior can be customized with Awaitable Customizations.

3rd Party / Unknown Awaitables - It Just Works#

When you await or spawn an awaitable of a type that is unknown to TMC, that awaitable will automatically be wrapped into a tmc::task. This implements the executor and priority affinity, as well as compatibility with spawn*() functions, at a small performance cost. If you want to remove this overhead, see Integrating External Awaitables.