tmc::ex_manual_st#

An executor that does not own any threads. Work can be posted to this executor at any time, but it will only be executed when you call one of the run_*() functions. The caller of run_*() will execute work inline on the current thread.

It is safe to post work from any number of threads concurrently, but run_*() must only be called from 1 thread at a time.

This allows you to poll for continuations at a specific time, such as in the main loop of a game engine.

Usage Example#

This example uses ex_manual_st to poll for completions of both background work (which may complete at any time) and specific work which must be completed before we can proceed.

tmc::ex_manual_st mainLoopExecutor;

tmc::task<void> loadEntity(Entity& entity) {
  co_await readFile(...);
  parseFile(...); // CPU-bound work to be done on the multi-threaded ex_cpu
  co_await tmc::resume_on(mainLoopExecutor);
  spawnEntity(...); // must be done on main thread for sync reasons
}

int main() {
  tmc::cpu_executor().init();
  mainLoopExecutor.init();
  while (true) {
    // Distant entities can be loaded at any time. We don't need to wait for
    // them to finish.
    std::vector<Entity> distant = get_new_distant_entities();
    for (auto& e : distant) {
      tmc::post(tmc::cpu_executor(), loadEntity(e));
    }

    // Nearby entities must be loaded this frame.
    std::vector<Entity> nearby = get_new_nearby_entities();
    std::future<void> nearbyReady = tmc::post_bulk_waitable(
      tmc::cpu_executor(),
      nearby | std::ranges::views::transform(loadEntity)
    );

    // Continue polling until all nearby entities are loaded.
    // If any distant entities are ready, they will be processed too.
    while (nearbyReady.wait_for(std::chrono::seconds(0)) !=
          std::future_status::ready) {
      mainLoopExecutor.run_all();
    }

    render();
  }
}

API Reference#

class ex_manual_st#

An executor that does not own any threads. Work can be posted to this executor at any time, but it will only be executed when you call one of the run_*() functions. The caller of run_*() will execute work inline on the current thread.

It is safe to post work from any number of threads concurrently, but run_*() must only be called from 1 thread at a time.

Public Functions

bool run_one()#

Attempt to run 1 work item from the executor’s queue. Work items will be executed on the current thread. Returns true if any work was waiting, and 1 work item was executed. Returns false if the executor’s queue was empty.

size_t run_all()#

Run all work items from the executor’s queue. Work items will be executed on the current thread. Returns the number of work items that were executed (0 if it was empty).

The returned count may be larger than the number of work items originally posted, because awaitables may resume suspended tasks by posting them back to the executor queue.

size_t run_n(const size_t MaxCount)#

Run up to MaxCount work items from the executor’s queue. Work items will be executed on the current thread. Returns the number of work items that were executed (0 if it was empty). MaxCount must be non-zero.

The returned count may be larger than the number of work items originally posted, because awaitables may resume suspended tasks by posting them back to the executor queue.

bool empty()#

Returns true if the executor’s queue appears to be empty at the current moment.

ex_manual_st &set_priority_count(size_t PriorityCount)#

Builder func to set the number of priority levels before calling init(). The value must be in the range [1, 16]. The default is 1.

size_t priority_count()#

Gets the number of priority levels. Only useful after init() has been called.

void init()#

Initializes the executor. If you want to customize the behavior, call the set_X() functions before calling init(). By default, uses hwloc to automatically generate threads, and creates 1 (or TMC_PRIORITY_COUNT) priority levels.

If the executor is already initialized, calling init() will do nothing.

void teardown()#

Stops the executor and destroys resources. Does not wait for any queued work to complete.

Restores the executor to an uninitialized state. After calling teardown(), you may call set_X() to reconfigure the executor and call init() again.

If the executor is not initialized, calling teardown() will do nothing.

ex_manual_st()#

After constructing, you must call init() before use.

~ex_manual_st()#

Invokes teardown().

void post(work_item &&Item, size_t Priority = 0, size_t ThreadHint = NO_HINT)#

Submits a single work_item to the executor. If Priority is out of range, it will be clamped to an in-range value.

Rather than calling this directly, it is recommended to use the tmc::post() free function template.

tmc::ex_any *type_erased()#

Returns a pointer to the type erased ex_any version of this executor. This object shares a lifetime with this executor, and can be used for pointer-based equality comparison against the thread-local tmc::current_executor().

template<typename It>
inline void post_bulk(It &&Items, size_t Count, size_t Priority = 0, size_t ThreadHint = NO_HINT)#

Submits count items to the executor. It is expected to be an iterator type that implements operator*() and It& operator++(). If Priority is out of range, it will be clamped to an in-range value.

Rather than calling this directly, it is recommended to use the tmc::post_bulk() free function template.