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