Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
async_queue.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
6
7#include <concurrentqueue/moodycamel/concurrentqueue.h>
8
9#include <algorithm>
10#include <concepts>
11#include <cstddef>
12#include <iterator>
13#include <limits>
14#include <memory>
15#include <ranges>
16#include <utility>
17
18#ifdef HELIOS_STL_FLAT_MAP_AVAILABLE
19#include <flat_map>
20#else
21#include <boost/container/flat_map.hpp>
22#endif
23
24namespace helios::ecs {
25
26namespace details {
27
28/// @brief Base class for type-erased async message storage.
30public:
31 virtual ~AsyncMessageStorage() = default;
32
33 /// @brief Clears all messages from the queue.
34 virtual void Clear() = 0;
35
36 /**
37 * @brief Checks if the queue is empty.
38 * @return True if no messages are stored, false otherwise
39 */
40 [[nodiscard]] virtual bool Empty() const noexcept = 0;
41
42 /**
43 * @brief Gets approximate number of messages stored in the queue.
44 * @return Message count
45 */
46 [[nodiscard]] virtual size_t SizeApprox() const noexcept = 0;
47};
48
49} // namespace details
50
51using AsyncMessageQueueProducerToken = moodycamel::ProducerToken;
52using AsyncMessageQueueConsumerToken = moodycamel::ConsumerToken;
53
54/**
55 * @brief Typed async message storage backed by a lock-free concurrent queue.
56 * @note Thread-safe for concurrent enqueue/dequeue operations.
57 * @tparam T Async message type
58 */
59template <AsyncMessageTrait T>
60class TypedAsyncMessageStorage final : public details::AsyncMessageStorage {
61public:
65 ~TypedAsyncMessageStorage() override = default;
66
69 default;
70
71 /// @brief Clears all messages from the queue by draining it.
72 void Clear() override;
73
74 /**
75 * @brief Enqueues a single message into the queue.
76 * @param message Message to enqueue
77 */
78 void Enqueue(const T& message)
79 requires std::copy_constructible<T>
80 {
81 queue_.enqueue(T{message});
82 }
83
84 /**
85 * @brief Enqueues a single message into the queue.
86 * @param message Message to enqueue
87 */
88 void Enqueue(T&& message) { queue_.enqueue(std::move(message)); }
89
90 /**
91 * @brief Enqueues a single message into the queue using a producer token.
92 * @param token Producer token for improved throughput
93 * @param message Message to enqueue
94 */
95 void Enqueue(AsyncMessageQueueProducerToken& token, const T& message)
96 requires std::copy_constructible<T>
97 {
98 queue_.enqueue(token, T{message});
99 }
100
101 /**
102 * @brief Enqueues a single message into the queue using a producer token.
103 * @param token Producer token for improved throughput
104 * @param message Message to enqueue
105 */
106 void Enqueue(AsyncMessageQueueProducerToken& token, T&& message) {
107 queue_.enqueue(token, std::move(message));
108 }
109
110 /**
111 * @brief Enqueues multiple messages in bulk.
112 * @tparam R Range type
113 * @param messages Range of messages to enqueue
114 */
115 template <std::ranges::input_range R>
116 requires std::same_as<std::ranges::range_value_t<R>, T>
117 void EnqueueBulk(R&& messages);
118
119 /**
120 * @brief Enqueues multiple messages in bulk using a producer token.
121 * @tparam R Range type
122 * @param token Producer token for improved throughput
123 * @param messages Range of messages to enqueue
124 */
125 template <std::ranges::input_range R>
126 requires std::same_as<std::ranges::range_value_t<R>, T>
127 void EnqueueBulk(AsyncMessageQueueProducerToken& token, R&& messages);
128
129 /**
130 * @brief Dequeues a single message from the queue.
131 * @return Dequeued message or default-constructed `T` if the queue is empty
132 */
133 T Dequeue();
134
135 /**
136 * @brief Dequeues a single message from the queue.
137 * @param token Consumer token for improved throughput
138 * @return Dequeued message or default-constructed `T` if the queue is empty
139 */
140 T Dequeue(AsyncMessageQueueConsumerToken& token);
141
142 /**
143 * @brief Dequeues a single message from the queue.
144 * @param dest Reference to store the dequeued message
145 * @return True if an message was dequeued and stored in dest, false if the
146 * queue was empty (`dest` is unchanged)
147 */
148 bool Dequeue(T& dest) { return queue_.try_dequeue(dest); }
149
150 /**
151 * @brief Dequeues a single message from the queue.
152 * @param token Consumer token for improved throughput
153 * @param dest Reference to store the dequeued message
154 * @return True if an message was dequeued and stored in dest, false if the
155 * queue was empty (`dest` is unchanged)
156 */
158 return queue_.try_dequeue(token, dest);
159 }
160
161 /**
162 * @brief Moves messages into an output iterator (dequeues them).
163 * @tparam It Output iterator type
164 * @param out Output iterator to receive messages
165 * @param max_count Maximum number of messages to move (default: all messages)
166 * @return Number of messages actually dequeued
167 */
168 template <typename It>
169 requires std::output_iterator<It, T>
170 size_t Into(It out, size_t max_count = std::numeric_limits<size_t>::max());
171
172 /**
173 * @brief Moves messages into an output iterator using a consumer token.
174 * @tparam It Output iterator type
175 * @param token Consumer token for improved throughput
176 * @param out Output iterator to receive messages
177 * @param max_count Maximum number of messages to move (default: all messages)
178 * @return Number of messages actually dequeued
179 */
180 template <typename It>
181 requires std::output_iterator<It, T>
182 size_t Into(AsyncMessageQueueConsumerToken& token, It out,
183 size_t max_count = std::numeric_limits<size_t>::max());
184
185 /**
186 * @brief Gets the producer token for this queue.
187 * @return Producer token
188 */
192
193 /**
194 * @brief Gets the consumer token for this queue.
195 * @return Consumer token
196 */
200
201 /**
202 * @brief Checks if the queue is empty.
203 * @return True if no messages are stored, false otherwise
204 */
205 [[nodiscard]] bool Empty() const noexcept override {
206 return queue_.size_approx() == 0;
207 }
208
209 /**
210 * @brief Gets approximate number of messages stored in the queue.
211 * @return Message count
212 */
213 [[nodiscard]] size_t SizeApprox() const noexcept override {
214 return queue_.size_approx();
215 }
216
217private:
218 moodycamel::ConcurrentQueue<T> queue_;
219};
220
221template <AsyncMessageTrait T>
223 T temp;
224 while (queue_.try_dequeue(temp)) {
225 // Discard messages
226 }
227}
228
229template <AsyncMessageTrait T>
230template <std::ranges::input_range R>
231 requires std::same_as<std::ranges::range_value_t<R>, T>
233 size_t count = 0;
234 if constexpr (std::ranges::sized_range<R>) {
235 count = std::ranges::size(messages);
236 } else {
237 count = static_cast<size_t>(std::ranges::distance(messages));
238 }
239
240 if constexpr (std::ranges::contiguous_range<R>) {
241 queue_.enqueue_bulk(std::ranges::data(messages), count);
242 } else if constexpr (std::ranges::borrowed_range<R>) {
243 queue_.enqueue_bulk(std::ranges::begin(messages), count);
244 } else {
245 queue_.enqueue_bulk(std::make_move_iterator(std::ranges::begin(messages)),
246 count);
247 }
248}
249
250template <AsyncMessageTrait T>
251template <std::ranges::input_range R>
252 requires std::same_as<std::ranges::range_value_t<R>, T>
254 AsyncMessageQueueProducerToken& token, R&& messages) {
255 size_t count = 0;
256 if constexpr (std::ranges::sized_range<R>) {
257 count = std::ranges::size(messages);
258 } else {
259 count = static_cast<size_t>(std::ranges::distance(messages));
260 }
261
262 if constexpr (std::ranges::contiguous_range<R>) {
263 queue_.enqueue_bulk(token, std::ranges::data(messages), count);
264 } else if constexpr (std::ranges::borrowed_range<R>) {
265 queue_.enqueue_bulk(token, std::ranges::begin(messages), count);
266 } else {
267 queue_.enqueue_bulk(
268 token, std::make_move_iterator(std::ranges::begin(messages)), count);
269 }
270}
271
272template <AsyncMessageTrait T>
274 T result;
275 queue_.try_dequeue(result);
276 return result;
277}
278
279template <AsyncMessageTrait T>
282 T result;
283 queue_.try_dequeue(token, result);
284 return result;
285}
286
287template <AsyncMessageTrait T>
288template <typename It>
289 requires std::output_iterator<It, T>
290inline size_t TypedAsyncMessageStorage<T>::Into(It out, size_t max_count) {
291 size_t dequeued = 0;
292 T temp;
293 while (dequeued < max_count && queue_.try_dequeue(temp)) {
294 *out = std::move(temp);
295 ++out;
296 ++dequeued;
297 }
298 return dequeued;
299}
300
301template <AsyncMessageTrait T>
302template <typename It>
303 requires std::output_iterator<It, T>
305 AsyncMessageQueueConsumerToken& token, It out, size_t max_count) {
306 size_t dequeued = 0;
307 T temp;
308 while (dequeued < max_count && queue_.try_dequeue(token, temp)) {
309 *out = std::move(temp);
310 ++out;
311 ++dequeued;
312 }
313 return dequeued;
314}
315
316/**
317 * @brief Async queue for managing multiple message types via lock-free
318 * concurrent queues.
319 * @details Each registered async message type gets its own
320 * `TypedAsyncMessageStorage` backed by a `moodycamel::ConcurrentQueue`.
321 * @note Thread-safe.
322 */
324public:
325 using size_type = size_t;
326
327 AsyncMessageQueue() = default;
330 ~AsyncMessageQueue() = default;
331
332 AsyncMessageQueue& operator=(const AsyncMessageQueue&) = delete;
333 AsyncMessageQueue& operator=(AsyncMessageQueue&&) noexcept = default;
334
335 /**
336 * @brief Registers an async message type with the queue.
337 * @tparam T Async message type
338 */
339 template <AsyncMessageTrait T>
340 void Register();
341
342 /// @brief Clears all messages from the queue maintaining all registered
343 /// types.
344 void Clear();
345
346 /**
347 * @brief Clears messages of a specific type.
348 * @tparam T Async message type
349 */
350 template <AsyncMessageTrait T>
351 void Clear();
352
353 /// @brief Resets the queue by clearing all messages and unregistering all
354 /// message types.
355 void Reset() noexcept { messages_.clear(); }
356
357 /**
358 * @brief Resets the queue by clearing and unregistering a specific message
359 * type.
360 * @tparam T Async message type
361 */
362 template <AsyncMessageTrait T>
363 void Reset() noexcept {
364 messages_.erase(MessageTypeIndex::From<T>());
365 }
366
367 /**
368 * @brief Enqueues a single message into the queue.
369 * @warning Triggers assertion if message type is not registered.
370 * @tparam T Async message type
371 * @param message Message to enqueue
372 */
373 template <AsyncMessageTrait T>
374 void Enqueue(T&& message) {
375 TypedStorage<T>().Enqueue(std::forward<T>(message));
376 }
377
378 /**
379 * @brief Enqueues a single message using a producer token for improved
380 * throughput.
381 * @warning Triggers assertion if message type is not registered.
382 * @tparam T Async message type
383 * @param token Producer token
384 * @param message Message to enqueue
385 */
386 template <AsyncMessageTrait T>
387 void Enqueue(AsyncMessageQueueProducerToken& token, T&& message) {
388 TypedStorage<T>().Enqueue(token, std::forward<T>(message));
389 }
390
391 /**
392 * @brief Enqueues multiple messages in bulk.
393 * @warning Triggers assertion if message type is not registered.
394 * @tparam R Range type whose value_type satisfies `AsyncMessageTrait`
395 * @param messages Range of messages to enqueue
396 */
397 template <std::ranges::input_range R>
399 void EnqueueBulk(R&& messages);
400
401 /**
402 * @brief Enqueues multiple messages in bulk using a producer token.
403 * @warning Triggers assertion if message type is not registered.
404 * @tparam R Range type whose value_type satisfies `AsyncMessageTrait`
405 * @param token Producer token
406 * @param messages Range of messages to enqueue
407 */
408 template <std::ranges::input_range R>
410 void EnqueueBulk(AsyncMessageQueueProducerToken& token, R&& messages);
411
412 /**
413 * @brief Creates a producer token for a specific message type.
414 * @warning Triggers assertion if message type is not registered.
415 * @tparam T Async message type
416 * @return Producer token
417 */
418 template <AsyncMessageTrait T>
420 return TypedStorage<T>().MakeProducerToken();
421 }
422
423 /**
424 * @brief Creates a consumer token for a specific message type.
425 * @warning Triggers assertion if message type is not registered.
426 * @tparam T Async message type
427 * @return Consumer token
428 */
429 template <AsyncMessageTrait T>
431 return TypedStorage<T>().MakeConsumerToken();
432 }
433
434 /**
435 * @brief Swaps the contents of this queue with another.
436 * @param other Another AsyncMessageQueue to swap with
437 */
438 void Swap(AsyncMessageQueue& other) noexcept {
439 std::swap(messages_, other.messages_);
440 }
441
442 /**
443 * @brief Swaps the contents of two AsyncMessageQueues.
444 * @param lhs First AsyncMessageQueue
445 * @param rhs Second AsyncMessageQueue
446 */
447 friend void swap(AsyncMessageQueue& lhs, AsyncMessageQueue& rhs) noexcept {
448 lhs.Swap(rhs);
449 }
450
451 /**
452 * @brief Checks if an message type is registered.
453 * @tparam T Async message type
454 * @return True if the message type is registered, false otherwise
455 */
456 template <AsyncMessageTrait T>
457 [[nodiscard]] bool IsRegistered() const noexcept {
458 return messages_.contains(MessageTypeIndex::From<T>());
459 }
460
461 /**
462 * @brief Checks if any registered type has messages.
463 * @return True if at least one message exists across all types, false
464 * otherwise
465 */
466 [[nodiscard]] bool HasMessages() const noexcept {
467 return std::ranges::any_of(
468 messages_, [](const auto& pair) { return !pair.second->Empty(); });
469 }
470
471 /**
472 * @brief Checks if messages of a specific type exist in the queue.
473 * @tparam T Async message type
474 * @return True if messages of type T exist, false otherwise or if the type is
475 * not registered
476 */
477 template <AsyncMessageTrait T>
478 [[nodiscard]] bool HasMessages() const noexcept;
479
480 /**
481 * @brief Gets the number of registered message types.
482 * @return Number of distinct message types
483 */
484 [[nodiscard]] size_type TypeCount() const noexcept {
485 return messages_.size();
486 }
487
488 /**
489 * @brief Gets the approximate total number of messages across all types.
490 * @return Approximate total message count
491 */
492 [[nodiscard]] size_type MessageCount() const noexcept;
493
494 /**
495 * @brief Gets the approximate number of messages for a specific type.
496 * @tparam T Async message type
497 * @return Approximate message count or 0 if the type is not registered
498 */
499 template <AsyncMessageTrait T>
500 [[nodiscard]] size_type MessageCount() const noexcept;
501
502 /**
503 * @brief Gets the typed storage for a specific message type.
504 * @warning Triggers assertion if type is not registered.
505 * @tparam T Async message type
506 * @return Reference to the typed storage
507 */
508 template <AsyncMessageTrait T>
509 [[nodiscard]] auto TypedStorage() noexcept -> TypedAsyncMessageStorage<T>&;
510
511 /**
512 * @brief Gets the typed storage for a specific message type (const).
513 * @warning Triggers assertion if type is not registered.
514 * @tparam T Async message type
515 * @return Const reference to the typed storage
516 */
517 template <AsyncMessageTrait T>
518 [[nodiscard]] auto TypedStorage() const noexcept
519 -> const TypedAsyncMessageStorage<T>&;
520
521private:
522#ifdef HELIOS_STL_FLAT_MAP_AVAILABLE
523 using MessageStorage =
524 std::flat_map<MessageTypeIndex,
525 std::unique_ptr<details::AsyncMessageStorage>>;
526#else
527 using MessageStorage =
528 boost::container::flat_map<MessageTypeIndex,
529 std::unique_ptr<details::AsyncMessageStorage>>;
530#endif
531
532 MessageStorage messages_; ///< Storage for async messages of different types
533};
534
535template <AsyncMessageTrait T>
537 if (!IsRegistered<T>()) [[likely]] {
538 messages_.emplace(MessageTypeIndex::From<T>(),
539 std::make_unique<TypedAsyncMessageStorage<T>>());
540 }
541}
542
544 for (auto&& [_, storage] : messages_) {
545 storage->Clear();
546 }
547}
548
549template <AsyncMessageTrait T>
551 const auto it = messages_.find(MessageTypeIndex::From<T>());
552 if (it == messages_.end()) [[unlikely]] {
553 return;
554 }
555 static_cast<TypedAsyncMessageStorage<T>&>(*it->second).Clear();
556}
557
558template <std::ranges::input_range R>
560inline void AsyncMessageQueue::EnqueueBulk(R&& messages) {
561 using T = std::ranges::range_value_t<R>;
562 HELIOS_ASSERT(IsRegistered<T>(), "Async message type '{}' is not registered!",
564 TypedStorage<T>().EnqueueBulk(std::forward<R>(messages));
565}
566
567template <std::ranges::input_range R>
570 AsyncMessageQueueProducerToken& token, R&& messages) {
571 using T = std::ranges::range_value_t<R>;
572 HELIOS_ASSERT(IsRegistered<T>(), "Async message type '{}' is not registered!",
574 TypedStorage<T>().EnqueueBulk(token, std::forward<R>(messages));
575}
576
577template <AsyncMessageTrait T>
578inline bool AsyncMessageQueue::HasMessages() const noexcept {
579 const auto it = messages_.find(MessageTypeIndex::From<T>());
580 if (it == messages_.end()) [[unlikely]] {
581 return false;
582 }
583 return !static_cast<const TypedAsyncMessageStorage<T>&>(*it->second).Empty();
584}
585
586inline auto AsyncMessageQueue::MessageCount() const noexcept -> size_type {
587 size_type total = 0;
588 for (const auto& [_, storage] : messages_) {
589 total += storage->SizeApprox();
590 }
591 return total;
592}
593
594template <AsyncMessageTrait T>
595inline auto AsyncMessageQueue::MessageCount() const noexcept -> size_type {
596 const auto it = messages_.find(MessageTypeIndex::From<T>());
597 if (it == messages_.end()) [[unlikely]] {
598 return 0;
599 }
600 return static_cast<const TypedAsyncMessageStorage<T>&>(*it->second)
601 .SizeApprox();
602}
603
604template <AsyncMessageTrait T>
607 HELIOS_ASSERT(IsRegistered<T>(), "Async message type '{}' is not registered!",
609 return static_cast<TypedAsyncMessageStorage<T>&>(
610 *messages_.at(MessageTypeIndex::From<T>()));
611}
612
613template <AsyncMessageTrait T>
614inline auto AsyncMessageQueue::TypedStorage() const noexcept
615 -> const TypedAsyncMessageStorage<T>& {
616 HELIOS_ASSERT(IsRegistered<T>(), "Async message type '{}' is not registered!",
618 return static_cast<const TypedAsyncMessageStorage<T>&>(
619 *messages_.at(MessageTypeIndex::From<T>()));
620}
621
622} // namespace helios::ecs
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
void Clear()
Clears all messages from the queue maintaining all registered types.
void Register()
Registers an async message type with the queue.
auto TypedStorage() noexcept -> TypedAsyncMessageStorage< T > &
Gets the typed storage for a specific message type.
AsyncMessageQueueProducerToken MakeProducerToken()
Creates a producer token for a specific message type.
bool IsRegistered() const noexcept
Checks if an message type is registered.
size_type TypeCount() const noexcept
Gets the number of registered message types.
AsyncMessageQueue(AsyncMessageQueue &&) noexcept=default
void Reset() noexcept
Resets the queue by clearing and unregistering a specific message type.
void EnqueueBulk(R &&messages)
Enqueues multiple messages in bulk.
void Enqueue(T &&message)
Enqueues a single message into the queue.
void Enqueue(AsyncMessageQueueProducerToken &token, T &&message)
Enqueues a single message using a producer token for improved throughput.
friend void swap(AsyncMessageQueue &lhs, AsyncMessageQueue &rhs) noexcept
Swaps the contents of two AsyncMessageQueues.
bool HasMessages() const noexcept
Checks if any registered type has messages.
AsyncMessageQueue(const AsyncMessageQueue &)=delete
void Swap(AsyncMessageQueue &other) noexcept
Swaps the contents of this queue with another.
size_type MessageCount() const noexcept
Gets the approximate total number of messages across all types.
AsyncMessageQueueConsumerToken MakeConsumerToken()
Creates a consumer token for a specific message type.
void Reset() noexcept
Resets the queue by clearing all messages and unregistering all message types.
Typed async message storage backed by a lock-free concurrent queue.
void Enqueue(const T &message)
Enqueues a single message into the queue.
bool Dequeue(T &dest)
Dequeues a single message from the queue.
bool Dequeue(AsyncMessageQueueConsumerToken &token, T &dest)
Dequeues a single message from the queue.
size_t Into(It out, size_t max_count=std::numeric_limits< size_t >::max())
Moves messages into an output iterator (dequeues them).
T Dequeue()
Dequeues a single message from the queue.
void Enqueue(AsyncMessageQueueProducerToken &token, T &&message)
Enqueues a single message into the queue using a producer token.
AsyncMessageQueueConsumerToken MakeConsumerToken()
Gets the consumer token for this queue.
void Enqueue(AsyncMessageQueueProducerToken &token, const T &message)
Enqueues a single message into the queue using a producer token.
AsyncMessageQueueProducerToken MakeProducerToken()
Gets the producer token for this queue.
TypedAsyncMessageStorage(TypedAsyncMessageStorage &&) noexcept=default
void EnqueueBulk(R &&messages)
Enqueues multiple messages in bulk.
void Clear() override
Clears all messages from the queue by draining it.
TypedAsyncMessageStorage(const TypedAsyncMessageStorage &)=delete
void Enqueue(T &&message)
Enqueues a single message into the queue.
bool Empty() const noexcept override
Checks if the queue is empty.
size_t SizeApprox() const noexcept override
Gets approximate number of messages stored in the queue.
Base class for type-erased async message storage.
virtual void Clear()=0
Clears all messages from the queue.
virtual bool Empty() const noexcept=0
Checks if the queue is empty.
virtual size_t SizeApprox() const noexcept=0
Gets approximate number of messages stored in the queue.
static constexpr TypeIndex From() noexcept
Constructs a TypeIndex from a type T.
Concept for valid async message types.
Definition message.hpp:44
moodycamel::ConsumerToken AsyncMessageQueueConsumerToken
moodycamel::ProducerToken AsyncMessageQueueProducerToken
utils::TypeIndex MessageTypeIndex
Type index for messages.
Definition message.hpp:14
constexpr std::string_view MessageNameOf() noexcept
Gets the name of an message.
Definition message.hpp:97
STL namespace.