Job System#

Hush Engine provides a coroutine-based job system built on C++20 coroutines and a work-stealing thread pool. It allows you to schedule and execute tasks concurrently without managing threads directly.

The Engine ThreadPool#

The engine owns a ThreadPool instance that is created during initialization. It uses std::thread::hardware_concurrency() threads, each pinned to a CPU core. You should use this pool for all your concurrent work rather than creating your own.

Access it through the engine pointer:

Hush::Threading::Executors::ThreadPool *pool = engine->GetEngineThreadPool();

The pool is passed to subsystems like Scene automatically. If you’re writing a system that receives the engine pointer, use GetEngineThreadPool() to obtain the pool.

Quick Start#

Here is a minimal example that schedules a task on the engine’s thread pool:

#include <async/Task.hpp>
#include <async/Executor.hpp>
#include <async/SyncWait.hpp>

using namespace Hush::Threading;
using namespace Hush::Threading::Executors;

// Define a coroutine that returns Task<void>
auto myTask = [](bool &result) -> Task<void> {
    result = true;
    co_return;
};

// Get the engine's thread pool
ThreadPool *pool = engine->GetEngineThreadPool();

bool done = false;

// Schedule the task on the pool
Task<void> task = RunOn(pool, myTask(done));

// Block until the task completes
Wait(task);

// done is now true

Tasks#

Task<T> is the coroutine return type used throughout the job system. It is a move-only type that represents a deferred computation.

  • Task<void> – a task that produces no result.

  • Task<int> – a task that produces an int value.

  • Task<T&> – a task that produces a reference.

Create a task by writing a coroutine function that returns Task<T>:

Task<int> ComputeValue(int input) {
    co_return input * 2;
}

Task<void> DoWork() {
    int result = co_await ComputeValue(21);
    // result == 42
    co_return;
}

You can check if a task has completed with Ready():

Task<void> task = RunOn(pool, DoWork());
// task.Ready() returns true once the coroutine has finished

Scheduling Tasks#

RunOn#

RunOn schedules a task on an executor (e.g., the thread pool) and returns a Task<void> that can be awaited or passed to Wait():

auto taskFunc = []() -> Task<void> {
    // This runs on a thread pool worker
    co_return;
};

Task<void> task = RunOn(pool, taskFunc());

// Either await it from another coroutine:
co_await task;

// Or block from non-coroutine code:
Wait(task);

SpawnOn#

SpawnOn is the fire-and-forget variant. The task will execute on the pool but you cannot await its completion or retrieve its result:

auto logTask = [](std::string_view msg) -> Task<void> {
    Hush::LogInfo(msg);
    co_return;
};

SpawnOn(pool, logTask("Background work done"));

Use SpawnOn for non-critical side effects like logging or telemetry.

Synchronization#

Wait#

Wait synchronously blocks the calling thread until an awaitable completes. Use it to bridge between non-coroutine code (e.g., main()) and the coroutine world:

Task<void> task = RunOn(pool, DoWork());
Wait(task); // Blocks until DoWork() finishes

Warning

Do not call Wait() from inside a coroutine. This will block a worker thread and may cause a deadlock. Use co_await instead.

WhenAll#

WhenAll waits for multiple tasks to complete. It supports both variadic and range-based usage:

// Range-based: pass a vector of tasks
std::vector<Task<void>> tasks;
for (int i = 0; i < 4; ++i) {
    tasks.push_back(RunOn(pool, taskFunc()));
}
Wait(WhenAll(std::move(tasks)));

Since tasks run in parallel, the total time is approximately the duration of the longest task, not the sum of all tasks.

ParallelFor#

ParallelFor splits an iterator range into chunks and processes them in parallel. It handles partitioning automatically:

  • Maximum 1024 tasks to avoid overwhelming the pool.

  • Minimum 1000 elements per chunk.

#include <utils/ParallelUtils.hpp>

std::vector<int> data(10000);
for (int i = 0; i < 10000; ++i) {
    data[i] = i;
}

// Increment every element in parallel
Wait(ParallelFor(pool, data.begin(), data.end(), [](int &value) {
    value += 1;
}));

// data[0] == 1, data[1] == 2, ...

ParallelFor returns a Task<void> that must be awaited or passed to Wait().

Best Practices#

  • Use the engine’s thread pool. Don’t create your own ThreadPool unless you have a specific reason to isolate work from the rest of the engine.

  • Use ``co_await`` inside coroutines, ``Wait()`` outside. Calling Wait() inside a coroutine blocks a worker thread and risks deadlock.

  • Prefer ``ParallelFor`` for data-parallel work over manually spawning tasks with a loop.

  • Use ``WhenAll`` to wait for multiple tasks rather than calling Wait() on each one sequentially.

  • Keep tasks independent. Avoid shared mutable state between tasks. If you must share state, use proper synchronization (e.g., std::mutex).

ThreadPool (Advanced)#

For advanced use cases where you need a separate pool (e.g., isolating I/O-bound work), you can create one manually:

#include <executors/ThreadPool.hpp>

using namespace Hush::Threading::Executors;

ThreadPoolOptions options;
options.numThreads = 4;
options.pinToCore = false;

ThreadPool pool = ThreadPool::Create(options);

// Use it like the engine pool
Wait(RunOn(&pool, myTask()));

ThreadPoolOptions fields:

  • numThreads – Number of worker threads. Defaults to std::thread::hardware_concurrency().

  • pinToCore – Whether to pin each thread to a CPU core. Defaults to false.

ThreadPool is non-copyable and non-movable. It must outlive all tasks scheduled on it.

API Reference#

struct ThreadPoolOptions

Public Members

uint32_t numThreads = std::thread::hardware_concurrency()
bool pinToCore = false
class ThreadPool

Public Functions

ThreadPool(const ThreadPool &other) = delete
ThreadPool &operator=(const ThreadPool &other) = delete
ThreadPool(ThreadPool &&other) noexcept = delete
ThreadPool &operator=(ThreadPool &&other) noexcept = delete
ThreadPool() = delete
~ThreadPool()
TaskOperation Schedule()
inline uint32_t GetNumThreads() const noexcept

Public Static Functions

static ThreadPool Create(ThreadPoolOptions options = ThreadPoolOptions())