IceShard 1
A personal game engine project, with development focused on 2D/2.5D games.
Loading...
Searching...
No Matches
atomic_linked_queue.hxx
Go to the documentation of this file.
1
3
4#pragma once
6#include <atomic>
7
8namespace ice
9{
10
11 template<ice::concepts::LinkedListNode NodeType>
13
14 template<ice::concepts::LinkedListNode NodeType>
16 {
17 using ValueType = NodeType;
18
19 std::atomic<NodeType*> _head;
20 std::atomic<NodeType*> _tail;
21
22 constexpr AtomicLinkedQueue() noexcept;
23 constexpr ~AtomicLinkedQueue() noexcept = default;
24
25 constexpr AtomicLinkedQueue(AtomicLinkedQueue&& other) noexcept;
26 constexpr AtomicLinkedQueue(AtomicLinkedQueue const& other) noexcept = delete;
27
28 constexpr auto operator=(AtomicLinkedQueue&& other) noexcept -> AtomicLinkedQueue&;
29 constexpr auto operator=(AtomicLinkedQueue const& other) noexcept -> AtomicLinkedQueue& = delete;
30
31 constexpr bool is_empty() const noexcept { return _head.load(std::memory_order_relaxed) == nullptr; }
32 constexpr bool not_empty() const noexcept { return is_empty() == false; }
33
34 template<ice::concepts::LinkedListNode DerivedNodeType = NodeType>
35 constexpr void push_back(DerivedNodeType* node) noexcept;
36 template<ice::concepts::LinkedListNode DerivedNodeType = NodeType>
37 constexpr bool push_back(ice::AtomicLinkedQueueRange<DerivedNodeType> range) noexcept;
38
39 [[nodiscard]]
40 constexpr auto take_front() noexcept -> NodeType*;
41 [[nodiscard]]
42 constexpr auto take_all() noexcept -> ice::AtomicLinkedQueueRange<NodeType>;
43 };
44
45 template<ice::concepts::LinkedListNode NodeType>
47 {
48 using ValueType = NodeType;
49
50 NodeType* _head = nullptr;
51 NodeType* _tail = nullptr;
52
53 struct Iterator
54 {
55 NodeType* _current;
56 NodeType* _next;
57 NodeType* _tail;
58
59 constexpr auto operator*() const noexcept -> NodeType*;
60 constexpr void operator++() noexcept;
61
62 constexpr bool operator==(Iterator other) const noexcept;
63 constexpr bool operator!=(Iterator other) const noexcept;
64 };
65
66 constexpr auto begin() const noexcept -> Iterator;
67 constexpr auto end() const noexcept -> Iterator;
68
69 constexpr bool is_empty() const noexcept { return _head == nullptr; }
70 constexpr bool not_empty() const noexcept { return is_empty() == false; }
71 };
72
73 template<ice::concepts::LinkedListNode NodeType>
75 : _head{ }
76 , _tail{ }
77 {
78 }
79
80 template<ice::concepts::LinkedListNode NodeType>
82 : _head{ }
83 , _tail{ }
84 {
85 // We move atomically to this object
86 ice::AtomicLinkedQueueRange<NodeType> range = other.take_all();
87 _head = range._head;
88 _tail = range._tail;
89 }
90
91 template<ice::concepts::LinkedListNode NodeType>
93 {
94 if (this != &other)
95 {
96 // We move atomically to this object
97 ice::AtomicLinkedQueueRange<NodeType> range = other.take_all();
98 _head = range._head;
99 _tail = range._tail;
100 }
101 return *this;
102 }
103
104 template<ice::concepts::LinkedListNode NodeType>
105 template<ice::concepts::LinkedListNode DerivedNodeType>
106 inline constexpr void AtomicLinkedQueue<NodeType>::push_back(DerivedNodeType* node) noexcept
107 {
108 NodeType* const previous_tail = _tail.exchange(node, std::memory_order_relaxed);
109
110 if (previous_tail == nullptr)
111 {
112 _head.store(node, std::memory_order_relaxed);
113 }
114 else
115 {
116 previous_tail->_next = node;
117 }
118
119 std::atomic_thread_fence(std::memory_order_release);
120 }
121
122 template<ice::concepts::LinkedListNode NodeType>
123 template<ice::concepts::LinkedListNode DerivedNodeType>
126 ) noexcept
127 {
128 // If no TAIL, then the range is empty
129 if (range._tail == nullptr)
130 {
131 return false;
132 }
133
134 // If we have a TAIL we need to also have a HEAD.
135 ICE_ASSERT_CORE(range._head != nullptr);
136
137 // Appending a range is a simple as adding one node.
138 // - We take the tail of the range and push it as the new tail on the queue
139 // - We set the previous_tail->next pointer to the pushed range head or set is as the new head.
140 // - All other values are still intact and the operation is still atomic like it was in the single node case.
141
142 NodeType* const previous_tail = _tail.exchange(range._tail, std::memory_order_relaxed);
143
144 if (previous_tail == nullptr)
145 {
146 _head.store(range._head, std::memory_order_relaxed);
147 }
148 else
149 {
150 previous_tail->_next = range._head;
151 }
152
153 std::atomic_thread_fence(std::memory_order_release);
154 return true;
155 }
156
157 template<ice::concepts::LinkedListNode NodeType>
158 inline constexpr auto AtomicLinkedQueue<NodeType>::take_front() noexcept -> NodeType*
159 {
160 NodeType* volatile result_node = _head.exchange(nullptr, std::memory_order_relaxed);
161
162 if (result_node == nullptr)
163 {
164 // TODO: Consider if we should spin 100 tries to get something if tail != nullptr.
165 // or let the caller always handle this case?
166 }
167
168 if (result_node != nullptr)
169 {
170 NodeType* tail_node = _tail.load(std::memory_order_relaxed);
171
172 // We only require a tail update if the result node (head) is different from the tail.
173 bool skip_tail_update = tail_node == result_node;
174 if (skip_tail_update)
175 {
176 // If tail and head are same we try to set the tail to nullptr.
177 // This indicates during a push that the head needs to be set again.
178 skip_tail_update = _tail.compare_exchange_strong(
179 tail_node,
180 nullptr,
181 std::memory_order_relaxed,
182 std::memory_order_relaxed
183 );
184 }
185
186 // If we failed the exchange, this means that a tail exists.
187 if (skip_tail_update == false)
188 {
189 // It might happen that the 'result_node' (former head) has it's '_next' pointer not yet set,
190 // requiring us to wait for the new 'head' pointer to be set from another thread.
191 NodeType volatile* next = result_node;
192 while (next->_next == nullptr)
193 {
194 std::atomic_thread_fence(std::memory_order_acquire);
195 }
196
197 NodeType* previous = _head.exchange(next->_next, std::memory_order_relaxed);
198 // A '_head' pointer should never have value set during a 'take_front' operation.
199 ICE_ASSERT_CORE(previous == nullptr);
200 }
201 }
202 return result_node;
203 }
204
205 template<ice::concepts::LinkedListNode NodeType>
206 inline constexpr auto AtomicLinkedQueue<NodeType>::take_all() noexcept -> ice::AtomicLinkedQueueRange<NodeType>
207 {
208 ice::AtomicLinkedQueueRange<NodeType> result{ ._head = nullptr, ._tail = nullptr };
209
210 NodeType* const head_result = _head.exchange(nullptr, std::memory_order_relaxed);
211
212 // If a value existed at the '_head' that means when we take the current '_tail'
213 // all values are explicitly accessible only to us!
214 if (head_result != nullptr)
215 {
216 result._head = head_result;
217 result._tail = _tail.exchange(nullptr, std::memory_order_acquire);
218 ICE_ASSERT_CORE(result._tail != nullptr);
219 }
220
221 return result;
222 }
223
224 template<ice::concepts::LinkedListNode NodeType>
225 inline constexpr auto AtomicLinkedQueueRange<NodeType>::Iterator::operator*() const noexcept -> NodeType*
226 {
227 return _current;
228 }
229
230 template<ice::concepts::LinkedListNode NodeType>
232 {
233 if (_current != _tail)
234 {
235 ICE_ASSERT_CORE(_current != nullptr);
236 _current = _next;
237
238 if (_current != _tail)
239 {
240 // TODO: Could be removed in non atomic linked queues become a thing.
241 // NOTE: Because we know that the 'next->next' pointer might change value from a different thread,
242 // it needs to be marked as volatile.
243 volatile NodeType* next = _next;
244 while (next->_next == nullptr)
245 {
246 std::atomic_thread_fence(std::memory_order_acquire);
247 }
248
249 _next = next->_next;
250 }
251 }
252 else
253 {
255 _current = nullptr;
256 }
257 }
258
259 template<ice::concepts::LinkedListNode NodeType>
260 inline constexpr bool AtomicLinkedQueueRange<NodeType>::Iterator::operator==(Iterator other) const noexcept
261 {
262 return (_tail == other._tail) && (_current == other._current);
263 }
264
265 template<ice::concepts::LinkedListNode NodeType>
266 inline constexpr bool AtomicLinkedQueueRange<NodeType>::Iterator::operator!=(Iterator other) const noexcept
267 {
268 return !(*this == other);
269 }
270
271 template<ice::concepts::LinkedListNode NodeType>
272 inline constexpr auto AtomicLinkedQueueRange<NodeType>::begin() const noexcept -> Iterator
273 {
274 if (_head != nullptr)
275 {
276 if (_head == _tail)
277 {
278 return { _head, nullptr, _tail };
279 }
280
281 // NOTE: Wait for the first 'next' pointer to be set from a different thread.
282 NodeType volatile* next = _head;
283 while (next->_next == nullptr)
284 {
285 std::atomic_thread_fence(std::memory_order_acquire);
286 }
287
288 return { _head, next->_next, _tail };
289 }
290 else
291 {
292 return end();
293 }
294 }
295
296 template<ice::concepts::LinkedListNode NodeType>
297 inline constexpr auto AtomicLinkedQueueRange<NodeType>::end() const noexcept -> Iterator
298 {
299 return { nullptr, nullptr, _tail };
300 }
301
302} // namespace ice
#define ICE_ASSERT_CORE(expression)
Definition assert_core.hxx:43
Definition container_concepts.hxx:12
SPDX-License-Identifier: MIT.
Definition array.hxx:12
constexpr auto operator=(AtomicLinkedQueue &&other) noexcept -> AtomicLinkedQueue &
Definition atomic_linked_queue.hxx:92
constexpr bool is_empty() const noexcept
Definition atomic_linked_queue.hxx:31
std::atomic< NodeType * > _tail
Definition atomic_linked_queue.hxx:20
NodeType ValueType
Definition atomic_linked_queue.hxx:17
constexpr auto take_all() noexcept -> ice::AtomicLinkedQueueRange< NodeType >
Definition atomic_linked_queue.hxx:206
constexpr auto take_front() noexcept -> NodeType *
Definition atomic_linked_queue.hxx:158
constexpr AtomicLinkedQueue() noexcept
Definition atomic_linked_queue.hxx:74
constexpr bool not_empty() const noexcept
Definition atomic_linked_queue.hxx:32
std::atomic< NodeType * > _head
Definition atomic_linked_queue.hxx:19
constexpr void push_back(DerivedNodeType *node) noexcept
Definition atomic_linked_queue.hxx:106
Definition atomic_linked_queue.hxx:54
constexpr bool operator==(Iterator other) const noexcept
Definition atomic_linked_queue.hxx:260
NodeType * _next
Definition atomic_linked_queue.hxx:56
NodeType * _current
Definition atomic_linked_queue.hxx:55
constexpr void operator++() noexcept
Definition atomic_linked_queue.hxx:231
constexpr bool operator!=(Iterator other) const noexcept
Definition atomic_linked_queue.hxx:266
NodeType * _tail
Definition atomic_linked_queue.hxx:57
constexpr auto operator*() const noexcept -> NodeType *
Definition atomic_linked_queue.hxx:225
Definition atomic_linked_queue.hxx:47
NodeType ValueType
Definition atomic_linked_queue.hxx:48
constexpr bool is_empty() const noexcept
Definition atomic_linked_queue.hxx:69
NodeType * _tail
Definition atomic_linked_queue.hxx:51
constexpr bool not_empty() const noexcept
Definition atomic_linked_queue.hxx:70
constexpr auto end() const noexcept -> Iterator
Definition atomic_linked_queue.hxx:297
constexpr auto begin() const noexcept -> Iterator
Definition atomic_linked_queue.hxx:272
NodeType * _head
Definition atomic_linked_queue.hxx:50