Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
executor.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
5#include <helios/async/details/profile.hpp>
8
9#include <taskflow/core/async_task.hpp>
10#include <taskflow/core/executor.hpp>
11
12#include <algorithm>
13#include <concepts>
14#include <cstddef>
15#include <future>
16#include <ranges>
17#include <string>
18#include <type_traits>
19#include <utility>
20#include <vector>
21
22namespace helios::async {
23
24/**
25 * @brief Manages worker threads and executes task graphs using work-stealing
26 * scheduling.
27 * @details The Executor wraps `tf::Executor` and provides methods to run
28 * `TaskGraph`s and create asynchronous tasks. It manages a pool of worker
29 * threads that efficiently execute tasks using a work-stealing algorithm.
30 * @note All member functions are thread-safe unless otherwise noted.
31 */
32class Executor {
33public:
34 /**
35 * @brief Default constructs an executor with hardware concurrency worker
36 * threads.
37 */
38 Executor() = default;
39
40 /**
41 * @brief Constructs an executor with the specified number of worker threads.
42 * @param worker_thread_count Number of worker threads to create
43 */
44 explicit Executor(size_t worker_thread_count)
45 : executor_(worker_thread_count) {}
46 Executor(const Executor&) = delete;
47 Executor(Executor&&) = delete;
48 ~Executor() = default;
49
50 Executor& operator=(const Executor&) = delete;
52
53 /**
54 * @brief Runs a task graph once.
55 * @details The executor does not own the graph - ensure it remains alive
56 * during execution.
57 * @note Thread-safe.
58 * @param graph Task graph to execute
59 * @return Future that completes when execution finishes
60 */
61 auto Run(TaskGraph& graph) -> Future<void> {
62 return Future<void>(executor_.run(graph.UnderlyingTaskflow()));
63 }
64
65 /**
66 * @brief Runs a moved task graph once.
67 * @details The executor takes ownership of the moved graph.
68 * @note Thread-safe.
69 * @param graph Task graph to execute (moved)
70 * @return Future that completes when execution finishes
71 */
72 auto Run(TaskGraph&& graph) -> Future<void> {
73 return Future<void>(
74 executor_.run(std::move(std::move(graph).UnderlyingTaskflow())));
75 }
76
77 /**
78 * @brief Runs a task graph once and invokes a callback upon completion.
79 * @details The executor does not own the graph - ensure it remains alive
80 * during execution.
81 * @note Thread-safe.
82 * @tparam C Callable type
83 * @param graph Task graph to execute
84 * @param callable Callback to invoke after execution completes
85 * @return Future that completes when execution finishes
86 */
87 template <std::invocable C>
88 auto Run(TaskGraph& graph, C&& callable) -> Future<void> {
89 return Future<void>(
90 executor_.run(graph.UnderlyingTaskflow(), std::forward<C>(callable)));
91 }
92
93 /**
94 * @brief Runs a moved task graph once and invokes a callback upon completion.
95 * @details The executor takes ownership of the moved graph.
96 * @note Thread-safe.
97 * @tparam C Callable type
98 * @param graph Task graph to execute (moved)
99 * @param callable Callback to invoke after execution completes
100 * @return Future that completes when execution finishes
101 */
102 template <std::invocable C>
103 auto Run(TaskGraph&& graph, C&& callable) -> Future<void> {
104 return Future<void>(
105 executor_.run(std::move(std::move(graph).UnderlyingTaskflow()),
106 std::forward<C>(callable)));
107 }
108
109 /**
110 * @brief Runs a task graph for the specified number of times.
111 * @details The executor does not own the graph - ensure it remains alive
112 * during execution.
113 * @note Thread-safe.
114 * @param graph Task graph to execute
115 * @param count Number of times to run the graph
116 * @return Future that completes when all executions finish
117 */
118 auto RunN(TaskGraph& graph, size_t count) -> Future<void> {
119 return Future<void>(executor_.run_n(graph.UnderlyingTaskflow(), count));
120 }
121
122 /**
123 * @brief Runs a moved task graph for the specified number of times.
124 * @details The executor takes ownership of the moved graph.
125 * @note Thread-safe.
126 * @param graph Task graph to execute (moved)
127 * @param count Number of times to run the graph
128 * @return Future that completes when all executions finish
129 */
130 auto RunN(TaskGraph&& graph, size_t count) -> Future<void> {
131 return Future<void>(executor_.run_n(
132 std::move(std::move(graph).UnderlyingTaskflow()), count));
133 }
134
135 /**
136 * @brief Runs a task graph for the specified number of times and invokes a
137 * callback.
138 * @details The executor does not own the graph - ensure it remains alive
139 * during execution.
140 * @note Thread-safe.
141 * @tparam C Callable type
142 * @param graph Task graph to execute
143 * @param count Number of times to run the graph
144 * @param callable Callback to invoke after all executions complete
145 * @return Future that completes when all executions finish
146 */
147 template <std::invocable C>
148 auto RunN(TaskGraph& graph, size_t count, C&& callable) -> Future<void> {
149 return Future<void>(executor_.run_n(graph.UnderlyingTaskflow(), count,
150 std::forward<C>(callable)));
151 }
152
153 /**
154 * @brief Runs a moved task graph for the specified number of times and
155 * invokes a callback.
156 * @details The executor takes ownership of the moved graph.
157 * @note Thread-safe.
158 * @tparam C Callable type
159 * @param graph Task graph to execute (moved)
160 * @param count Number of times to run the graph
161 * @param callable Callback to invoke after all executions complete
162 * @return Future that completes when all executions finish
163 */
164 template <std::invocable C>
165 auto RunN(TaskGraph&& graph, size_t count, C&& callable) -> Future<void> {
166 return Future<void>(
167 executor_.run_n(std::move(std::move(graph).UnderlyingTaskflow()), count,
168 std::forward<C>(callable)));
169 }
170
171 /**
172 * @brief Runs a task graph repeatedly until the predicate returns true.
173 * @details The executor does not own the graph - ensure it remains alive
174 * during execution.
175 * @note Thread-safe.
176 * @tparam Predicate Predicate type
177 * @param graph Task graph to execute
178 * @param predicate Boolean predicate to determine when to stop
179 * @return Future that completes when predicate returns true
180 */
181 template <std::predicate Predicate>
182 auto RunUntil(TaskGraph& graph, Predicate&& predicate) -> Future<void> {
183 return Future<void>(executor_.run_until(
184 graph.UnderlyingTaskflow(), std::forward<Predicate>(predicate)));
185 }
186
187 /**
188 * @brief Runs a moved task graph repeatedly until the predicate returns true.
189 * @details The executor takes ownership of the moved graph.
190 * @note Thread-safe.
191 * @tparam Predicate Predicate type
192 * @param graph Task graph to execute (moved)
193 * @param predicate Boolean predicate to determine when to stop
194 * @return Future that completes when predicate returns true
195 */
196 template <std::predicate Predicate>
197 auto RunUntil(TaskGraph&& graph, Predicate&& predicate) -> Future<void> {
198 return Future<void>(
199 executor_.run_until(std::move(std::move(graph).UnderlyingTaskflow()),
200 std::forward<Predicate>(predicate)));
201 }
202
203 /**
204 * @brief Runs a task graph repeatedly until the predicate returns true, then
205 * invokes a callback.
206 * @details The executor does not own the graph - ensure it remains alive
207 * during execution.
208 * @note Thread-safe.
209 * @tparam Predicate Predicate type
210 * @tparam C Callable type
211 * @param graph Task graph to execute
212 * @param predicate Boolean predicate to determine when to stop
213 * @param callable Callback to invoke after execution completes
214 * @return Future that completes when predicate returns true and callback
215 * finishes
216 */
217 template <std::predicate Predicate, std::invocable C>
218 auto RunUntil(TaskGraph& graph, Predicate&& predicate, C&& callable)
219 -> Future<void> {
220 return Future<void>(executor_.run_until(graph.UnderlyingTaskflow(),
221 std::forward<Predicate>(predicate),
222 std::forward<C>(callable)));
223 }
224
225 /**
226 * @brief Runs a moved task graph repeatedly until the predicate returns true,
227 * then invokes a callback.
228 * @details The executor takes ownership of the moved graph.
229 * @note Thread-safe.
230 * @tparam Predicate Predicate type
231 * @tparam C Callable type
232 * @param graph Task graph to execute (moved)
233 * @param predicate Boolean predicate to determine when to stop
234 * @param callable Callback to invoke after execution completes
235 * @return Future that completes when predicate returns true and callback
236 * finishes
237 */
238 template <std::predicate Predicate, std::invocable C>
239 auto RunUntil(TaskGraph&& graph, Predicate&& predicate, C&& callable)
240 -> Future<void> {
241 return Future<void>(executor_.run_until(
242 std::move(std::move(graph).UnderlyingTaskflow()),
243 std::forward<Predicate>(predicate), std::forward<C>(callable)));
244 }
245
246 /**
247 * @brief Creates an asynchronous task that runs the given callable.
248 * @details The task is scheduled immediately and runs independently.
249 * @note Thread-safe.
250 * @tparam C Callable type
251 * @param callable Function to execute asynchronously
252 * @return Future that will hold the result of the execution
253 */
254 template <std::invocable C>
255 auto Async(C&& callable) -> std::future<std::invoke_result_t<C>> {
256 return executor_.async(std::forward<C>(callable));
257 }
258
259 /**
260 * @brief Creates a named asynchronous task that runs the given callable.
261 * @details The task is scheduled immediately and runs independently.
262 * @note Thread-safe.
263 * @tparam C Callable type
264 * @param name Name for the task (useful for debugging/profiling)
265 * @param callable Function to execute asynchronously
266 * @return Future that will hold the result of the execution
267 */
268 template <std::invocable C>
269 auto Async(std::string name, C&& callable)
270 -> std::future<std::invoke_result_t<C>>;
271
272 /**
273 * @brief Creates an asynchronous task without returning a future.
274 * @details More efficient than Async when you don't need the result.
275 * @note Thread-safe.
276 * @tparam C Callable type
277 * @param callable Function to execute asynchronously
278 */
279 template <std::invocable C>
280 void SilentAsync(C&& callable) {
281 executor_.silent_async(std::forward<C>(callable));
282 }
283
284 /**
285 * @brief Creates a named asynchronous task without returning a future.
286 * @details More efficient than Async when you don't need the result.
287 * @note Thread-safe.
288 * @tparam C Callable type
289 * @param name Name for the task (useful for debugging/profiling)
290 * @param callable Function to execute asynchronously
291 */
292 template <std::invocable C>
293 void SilentAsync(std::string name, C&& callable);
294
295 /**
296 * @brief Creates an asynchronous task that runs after specified dependencies
297 * complete.
298 * @details The task will only execute after all dependencies finish.
299 * @note Thread-safe.
300 * @tparam C Callable type
301 * @tparam Dependencies Range type containing AsyncTask dependencies
302 * @param callable Function to execute asynchronously
303 * @param dependencies Tasks that must complete before this task runs
304 * @return Pair containing AsyncTask handle and Future for the result
305 */
306 template <std::invocable C, std::ranges::range Dependencies>
307 requires std::same_as<std::ranges::range_value_t<Dependencies>, AsyncTask>
308 auto DependentAsync(C&& callable, const Dependencies& dependencies)
309 -> std::pair<AsyncTask, std::future<std::invoke_result_t<C>>>;
310
311 /**
312 * @brief Creates an asynchronous task that runs after dependencies complete,
313 * without returning a future.
314 * @details More efficient than DependentAsync when you don't need the result.
315 * @note Thread-safe.
316 * @tparam C Callable type
317 * @tparam Dependencies Range type containing AsyncTask dependencies
318 * @param callable Function to execute asynchronously
319 * @param dependencies Tasks that must complete before this task runs
320 * @return AsyncTask handle
321 */
322 template <std::invocable C, std::ranges::range Dependencies>
323 requires std::same_as<std::ranges::range_value_t<Dependencies>, AsyncTask>
324 AsyncTask SilentDependentAsync(C&& callable,
325 const Dependencies& dependencies);
326
327 /**
328 * @brief Blocks until all submitted tasks complete.
329 * @details Waits for all taskflows and async tasks to finish.
330 * @note Thread-safe.
331 */
332 void WaitForAll();
333
334 /**
335 * @brief Runs a task graph cooperatively and waits until it completes using
336 * the current worker thread.
337 * @warning Must be called from within a worker thread of this executor.
338 * Triggers assertion if called from a non-worker thread.
339 * @param graph Task graph to execute
340 */
341 void CoRun(TaskGraph& graph);
342
343 /**
344 * @brief Keeps the current worker thread running until the predicate returns
345 * true.
346 * @warning Must be called from within a worker thread of this executor.
347 * Triggers assertion if called from a non-worker thread.
348 * @tparam Predicate Predicate type
349 * @param predicate Boolean predicate to determine when to stop
350 */
351 template <std::predicate Predicate>
352 void CoRunUntil(Predicate&& predicate);
353
354 /**
355 * @brief Checks if the current thread is a worker thread of this executor.
356 * @note Thread safe.
357 * @return True if current thread is a worker, false otherwise
358 */
359 [[nodiscard]] bool IsWorkerThread() const { return CurrentWorkerId() != -1; }
360
361 /**
362 * @brief Gets the ID of the current worker thread.
363 * @note Thread-safe.
364 * @return Worker ID (0 to N-1) or -1 if not a worker thread
365 */
366 [[nodiscard]] int CurrentWorkerId() const {
367 return executor_.this_worker_id();
368 }
369
370 /**
371 * @brief Gets the total number of worker threads.
372 * @note Thread safe.
373 * @return Count of worker threads
374 */
375 [[nodiscard]] size_t WorkerCount() const noexcept {
376 return executor_.num_workers();
377 }
378
379 /**
380 * @brief Gets the number of worker threads currently waiting for work.
381 * @note Thread safe.
382 * @return Count of idle worker threads
383 */
384 [[nodiscard]] size_t IdleWorkerCount() const noexcept {
385 return executor_.num_waiters();
386 }
387
388 /**
389 * @brief Gets the number of task queues in the work-stealing scheduler.
390 * @note Thread safe.
391 * @return Count of task queues
392 */
393 [[nodiscard]] size_t QueueCount() const noexcept {
394 return executor_.num_queues();
395 }
396
397 /**
398 * @brief Gets the number of task graphs currently being executed.
399 * @note Thread safe.
400 * @return Count of running task graphs
401 */
402 [[nodiscard]] size_t RunningTopologyCount() const {
403 return executor_.num_topologies();
404 }
405
406private:
407 tf::Executor executor_;
408};
409
410template <std::invocable C>
411inline auto Executor::Async(std::string name, C&& callable)
412 -> std::future<std::invoke_result_t<C>> {
413 tf::TaskParams params;
414 params.name = std::move(name);
415 return executor_.async(params, std::forward<C>(callable));
416}
417
418template <std::invocable C>
419inline void Executor::SilentAsync(std::string name, C&& callable) {
420 tf::TaskParams params;
421 params.name = std::move(name);
422 executor_.silent_async(params, std::forward<C>(callable));
423}
424
425template <std::invocable C, std::ranges::range Dependencies>
426 requires std::same_as<std::ranges::range_value_t<Dependencies>, AsyncTask>
427inline auto Executor::DependentAsync(C&& callable,
428 const Dependencies& dependencies)
429 -> std::pair<AsyncTask, std::future<std::invoke_result_t<C>>> {
430 std::vector<tf::AsyncTask> tf_deps;
431 if constexpr (std::ranges::sized_range<Dependencies>) {
432 tf_deps.reserve(std::ranges::size(dependencies));
433 }
434
435 for (const auto& dep : dependencies) {
436 tf_deps.push_back(dep.UnderlyingTask());
437 }
438
439 auto [task, future] = executor_.dependent_async(
440 std::forward<C>(callable), tf_deps.begin(), tf_deps.end());
441 return std::make_pair(AsyncTask(std::move(task)), std::move(future));
442}
443
444template <std::invocable C, std::ranges::range Dependencies>
445 requires std::same_as<std::ranges::range_value_t<Dependencies>, AsyncTask>
447 C&& callable, const Dependencies& dependencies) {
448 std::vector<tf::AsyncTask> tf_deps;
449 if constexpr (std::ranges::sized_range<Dependencies>) {
450 tf_deps.reserve(std::ranges::size(dependencies));
451 }
452
453 for (const auto& dep : dependencies) {
454 tf_deps.push_back(dep.UnderlyingTask());
455 }
456
457 return AsyncTask(executor_.silent_dependent_async(
458 std::forward<C>(callable), tf_deps.begin(), tf_deps.end()));
459}
460
461inline void Executor::WaitForAll() {
462 HELIOS_ASYNC_PROFILE_SCOPE_N("helios::async::Executor::WaitForAll");
463 executor_.wait_for_all();
464}
465
466inline void Executor::CoRun(TaskGraph& graph) {
467 HELIOS_ASYNC_PROFILE_SCOPE_N("helios::async::Executor::CoRun");
468 HELIOS_ASSERT(IsWorkerThread(), "Must be called from a worker thread!");
469
470 executor_.corun(graph.UnderlyingTaskflow());
471}
472
473template <std::predicate Predicate>
474inline void Executor::CoRunUntil(Predicate&& predicate) {
475 HELIOS_ASYNC_PROFILE_SCOPE_N("helios::async::Executor::CoRunUntil");
476 HELIOS_ASSERT(IsWorkerThread(), "Must be called from a worker thread!");
477
478 executor_.corun_until(std::forward<Predicate>(predicate));
479}
480
481} // namespace helios::async
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
Handle to an asynchronous task managed by the Executor.
Executor(const Executor &)=delete
auto RunUntil(TaskGraph &graph, Predicate &&predicate, C &&callable) -> Future< void >
Runs a task graph repeatedly until the predicate returns true, then invokes a callback.
Definition executor.hpp:218
Executor & operator=(const Executor &)=delete
Executor & operator=(Executor &&)=delete
void WaitForAll()
Blocks until all submitted tasks complete.
Definition executor.hpp:461
auto Run(TaskGraph &&graph, C &&callable) -> Future< void >
Runs a moved task graph once and invokes a callback upon completion.
Definition executor.hpp:103
size_t IdleWorkerCount() const noexcept
Gets the number of worker threads currently waiting for work.
Definition executor.hpp:384
auto RunN(TaskGraph &graph, size_t count) -> Future< void >
Runs a task graph for the specified number of times.
Definition executor.hpp:118
auto RunUntil(TaskGraph &&graph, Predicate &&predicate) -> Future< void >
Runs a moved task graph repeatedly until the predicate returns true.
Definition executor.hpp:197
void CoRun(TaskGraph &graph)
Runs a task graph cooperatively and waits until it completes using the current worker thread.
Definition executor.hpp:466
auto RunN(TaskGraph &&graph, size_t count, C &&callable) -> Future< void >
Runs a moved task graph for the specified number of times and invokes a callback.
Definition executor.hpp:165
AsyncTask SilentDependentAsync(C &&callable, const Dependencies &dependencies)
Creates an asynchronous task that runs after dependencies complete, without returning a future.
Definition executor.hpp:446
auto RunN(TaskGraph &&graph, size_t count) -> Future< void >
Runs a moved task graph for the specified number of times.
Definition executor.hpp:130
bool IsWorkerThread() const
Checks if the current thread is a worker thread of this executor.
Definition executor.hpp:359
Executor(Executor &&)=delete
Executor()=default
Default constructs an executor with hardware concurrency worker threads.
void CoRunUntil(Predicate &&predicate)
Keeps the current worker thread running until the predicate returns true.
Definition executor.hpp:474
auto RunUntil(TaskGraph &&graph, Predicate &&predicate, C &&callable) -> Future< void >
Runs a moved task graph repeatedly until the predicate returns true, then invokes a callback.
Definition executor.hpp:239
size_t WorkerCount() const noexcept
Gets the total number of worker threads.
Definition executor.hpp:375
auto Run(TaskGraph &&graph) -> Future< void >
Runs a moved task graph once.
Definition executor.hpp:72
auto RunN(TaskGraph &graph, size_t count, C &&callable) -> Future< void >
Runs a task graph for the specified number of times and invokes a callback.
Definition executor.hpp:148
size_t RunningTopologyCount() const
Gets the number of task graphs currently being executed.
Definition executor.hpp:402
int CurrentWorkerId() const
Gets the ID of the current worker thread.
Definition executor.hpp:366
size_t QueueCount() const noexcept
Gets the number of task queues in the work-stealing scheduler.
Definition executor.hpp:393
Executor(size_t worker_thread_count)
Constructs an executor with the specified number of worker threads.
Definition executor.hpp:44
auto RunUntil(TaskGraph &graph, Predicate &&predicate) -> Future< void >
Runs a task graph repeatedly until the predicate returns true.
Definition executor.hpp:182
auto Run(TaskGraph &graph, C &&callable) -> Future< void >
Runs a task graph once and invokes a callback upon completion.
Definition executor.hpp:88
auto DependentAsync(C &&callable, const Dependencies &dependencies) -> std::pair< AsyncTask, std::future< std::invoke_result_t< C > > >
Creates an asynchronous task that runs after specified dependencies complete.
Definition executor.hpp:427
auto Run(TaskGraph &graph) -> Future< void >
Runs a task graph once.
Definition executor.hpp:61
void SilentAsync(C &&callable)
Creates an asynchronous task without returning a future.
Definition executor.hpp:280
auto Async(C &&callable) -> std::future< std::invoke_result_t< C > >
Creates an asynchronous task that runs the given callable.
Definition executor.hpp:255
Wrapper around tf::Future for handling asynchronous task results.
Definition future.hpp:22
Represents a task dependency graph that can be executed by an Executor.