Line data Source code
1 0 : // Distributed under the MIT License.
2 : // See LICENSE.txt for details.
3 :
4 : /*
5 : *The original code is distributed under the following copyright and license:
6 : *
7 : * Copyright (c) 2020 Erik Rigtorp <erik@rigtorp.se>
8 : *
9 : * Permission is hereby granted, free of charge, to any person obtaining a copy
10 : * of this software and associated documentation files (the "Software"), to deal
11 : * in the Software without restriction, including without limitation the rights
12 : * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 : * copies of the Software, and to permit persons to whom the Software is
14 : * furnished to do so, subject to the following conditions:
15 : *
16 : * The above copyright notice and this permission notice shall be included in
17 : * all copies or substantial portions of the Software.
18 : *
19 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 : * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 : * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 : * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 : * SOFTWARE.
26 : *
27 : *
28 : * SXS Modifications:
29 : * 1. Casing to match SpECTRE conventions
30 : * 2. Static capacity
31 : * 3. Storage is std::array
32 : * 4. Switch to west-const
33 : *
34 : */
35 :
36 : #pragma once
37 :
38 : #include <atomic>
39 : #include <cassert>
40 : #include <cstddef>
41 : #include <new> // Placement new
42 : #include <stdexcept>
43 : #include <type_traits>
44 :
45 : #include "Utilities/ErrorHandling/Assert.hpp"
46 : #include "Utilities/Requires.hpp"
47 :
48 : namespace Parallel {
49 : /*!
50 : * \brief A static capacity runtime-sized single-producer single-consumer
51 : * lockfree queue.
52 : *
53 : * As long as only one thread reads and writes simultaneously the queue is
54 : * threadsafe. Which threads read and write can change throughout program
55 : * execution, the important thing is that there is no instance during the
56 : * execution where more than one thread tries to read and where more than one
57 : * thread tries to write.
58 : *
59 : * \note This class is intentionally not serializable since handling
60 : * threadsafety around serialization requires careful thought of the individual
61 : * circumstances.
62 : */
63 : template <typename T, size_t Capacity>
64 1 : class StaticSpscQueue {
65 : private:
66 : #ifdef __cpp_lib_hardware_interference_size
67 : static constexpr size_t cache_line_size_ =
68 : std::hardware_destructive_interference_size;
69 : #else
70 0 : static constexpr size_t cache_line_size_ = 64;
71 : #endif
72 :
73 : // Padding to avoid false sharing between slots_ and adjacent allocations
74 0 : static constexpr size_t padding_ = (cache_line_size_ - 1) / sizeof(T) + 1;
75 :
76 : public:
77 0 : StaticSpscQueue() = default;
78 0 : ~StaticSpscQueue() {
79 : // Destruct objects in the buffer.
80 : while (front()) {
81 : pop();
82 : }
83 : }
84 :
85 0 : StaticSpscQueue(const StaticSpscQueue&) = delete;
86 0 : StaticSpscQueue& operator=(const StaticSpscQueue&) = delete;
87 0 : StaticSpscQueue(StaticSpscQueue&&) = delete;
88 0 : StaticSpscQueue& operator=(StaticSpscQueue&&) = delete;
89 :
90 : /// Construct a new element at the end of the queue in place.
91 : ///
92 : /// Uses placement new for in-place construction.
93 : ///
94 : /// \warning This may overwrite existing elements if `capacity()` is
95 : /// exceeded without warning.
96 : template <typename... Args>
97 1 : void emplace(Args&&... args) noexcept(
98 : std::is_nothrow_constructible_v<T, Args&&...>) {
99 : static_assert(std::is_constructible_v<T, Args&&...>,
100 : "T must be constructible with Args&&...");
101 : const auto write_index = write_index_.load(std::memory_order_relaxed);
102 : auto next_write_index = write_index + 1;
103 : if (next_write_index == capacity_) {
104 : next_write_index = 0;
105 : }
106 : while (next_write_index == read_index_cache_) {
107 : read_index_cache_ = read_index_.load(std::memory_order_acquire);
108 : }
109 : // Destroy the object this slot already holds (see `data_`).
110 : data_[write_index + padding_].~T();
111 : new (&data_[write_index + padding_]) T(std::forward<Args>(args)...);
112 : write_index_.store(next_write_index, std::memory_order_release);
113 : }
114 :
115 : /// Construct a new element at the end of the queue in place.
116 : ///
117 : /// Uses placement new for in-place construction.
118 : ///
119 : /// Returns `true` if the emplacement succeeded and `false` if it did
120 : /// not. If it failed then the queue is currently full.
121 : template <typename... Args>
122 1 : [[nodiscard]] bool try_emplace(Args&&... args) noexcept(
123 : std::is_nothrow_constructible_v<T, Args&&...>) {
124 : static_assert(std::is_constructible_v<T, Args&&...>,
125 : "T must be constructible with Args&&...");
126 : const auto write_index = write_index_.load(std::memory_order_relaxed);
127 : auto next_write_index = write_index + 1;
128 : if (next_write_index == capacity_) {
129 : next_write_index = 0;
130 : }
131 : if (next_write_index == read_index_cache_) {
132 : read_index_cache_ = read_index_.load(std::memory_order_acquire);
133 : if (next_write_index == read_index_cache_) {
134 : return false;
135 : }
136 : }
137 : // Destroy the object this slot already holds (see `data_`).
138 : data_[write_index + padding_].~T();
139 : new (&data_[write_index + padding_]) T(std::forward<Args>(args)...);
140 : write_index_.store(next_write_index, std::memory_order_release);
141 : return true;
142 : }
143 :
144 : /// Push a new element to the end of the queue.
145 : ///
146 : /// Uses `emplace()` internally.
147 : ///
148 : /// \warning This may overwrite existing elements if `capacity()` is
149 : /// exceeded without warning.
150 1 : void push(const T& v) noexcept(std::is_nothrow_copy_constructible_v<T>) {
151 : static_assert(std::is_copy_constructible_v<T>,
152 : "T must be copy constructible");
153 : emplace(v);
154 : }
155 :
156 : /// Push a new element to the end of the queue.
157 : ///
158 : /// Uses `emplace()` internally.
159 : ///
160 : /// \warning This may overwrite existing elements if `capacity()` is
161 : /// exceeded without warning.
162 : template <typename P, Requires<std::is_constructible_v<T, P&&>> = nullptr>
163 1 : void push(P&& v) noexcept(std::is_nothrow_constructible_v<T, P&&>) {
164 : emplace(std::forward<P>(v));
165 : }
166 :
167 : /// Push a new element to the end of the queue. Returns `false` if the queue
168 : /// is at capacity and does not push the new object, otherwise returns `true`.
169 : ///
170 : /// Uses `try_emplace()` internally.
171 1 : [[nodiscard]] bool try_push(const T& v) noexcept(
172 : std::is_nothrow_copy_constructible_v<T>) {
173 : static_assert(std::is_copy_constructible_v<T>,
174 : "T must be copy constructible");
175 : return try_emplace(v);
176 : }
177 :
178 : /// Push a new element to the end of the queue. Returns `false` if the queue
179 : /// is at capacity and does not push the new object, otherwise returns `true`.
180 : ///
181 : /// Uses `try_emplace()` internally.
182 : template <typename P, Requires<std::is_constructible_v<T, P&&>> = nullptr>
183 1 : [[nodiscard]] bool try_push(P&& v) noexcept(
184 : std::is_nothrow_constructible_v<T, P&&>) {
185 : return try_emplace(std::forward<P>(v));
186 : }
187 :
188 : /// Returns the first element from the queue.
189 : ///
190 : /// \note Returns `nullptr` if the queue is empty.
191 1 : [[nodiscard]] T* front() noexcept {
192 : const auto read_index = read_index_.load(std::memory_order_relaxed);
193 : if (read_index == write_index_cache_) {
194 : write_index_cache_ = write_index_.load(std::memory_order_acquire);
195 : if (write_index_cache_ == read_index) {
196 : return nullptr;
197 : }
198 : }
199 : return &data_[read_index + padding_];
200 : }
201 :
202 : /// Removes the first element from the queue.
203 1 : void pop() {
204 : static_assert(std::is_nothrow_destructible_v<T>,
205 : "T must be nothrow destructible");
206 : const auto read_index = read_index_.load(std::memory_order_relaxed);
207 : #ifdef SPECTRE_DEBUG
208 : const auto write_index = write_index_.load(std::memory_order_acquire);
209 : ASSERT(write_index != read_index,
210 : "Can't pop an element from an empty queue. read_index: "
211 : << read_index << " write_index " << write_index);
212 : #endif // SPECTRE_DEBUG
213 : // Leave a live object behind (see `data_`).
214 : data_[read_index + padding_].~T();
215 : new (&data_[read_index + padding_]) T{};
216 : auto next_read_index = read_index + 1;
217 : if (next_read_index == capacity_) {
218 : next_read_index = 0;
219 : }
220 : if (read_index == write_index_cache_) {
221 : write_index_cache_ = next_read_index;
222 : }
223 : read_index_.store(next_read_index, std::memory_order_release);
224 : }
225 :
226 : /// Returns the size of the queue at a particular hardware state.
227 : ///
228 : /// Note that while this can be checked in a threadsafe manner, it is up to
229 : /// the user to guarantee that another thread does not change the queue
230 : /// between when `size()` is called and how the result is used.
231 1 : [[nodiscard]] size_t size() const noexcept {
232 : std::ptrdiff_t diff = static_cast<std::ptrdiff_t>(
233 : write_index_.load(std::memory_order_acquire)) -
234 : static_cast<std::ptrdiff_t>(
235 : read_index_.load(std::memory_order_acquire));
236 : if (diff < 0) {
237 : diff += static_cast<std::ptrdiff_t>(capacity_);
238 : }
239 : return static_cast<size_t>(diff);
240 : }
241 :
242 : /// Returns `true` if the queue may be empty, otherwise `false`.
243 : ///
244 : /// Note that while this can be checked in a threadsafe manner, it is up to
245 : /// the user to guarantee that another thread does not change the queue
246 : /// between when `empty()` is called and how the result is used.
247 1 : [[nodiscard]] bool empty() const noexcept {
248 : return write_index_.load(std::memory_order_acquire) ==
249 : read_index_.load(std::memory_order_acquire);
250 : }
251 :
252 : /// Returns the capacity of the queue.
253 1 : [[nodiscard]] size_t capacity() const noexcept { return capacity_ - 1; }
254 :
255 : private:
256 0 : static constexpr size_t capacity_ = Capacity + 1;
257 : // Every slot holds a live object, so the array's destructor destroys each
258 : // element exactly once. `emplace` and `pop` maintain this.
259 0 : std::array<T, capacity_ + 2 * padding_> data_{};
260 :
261 : // Align to cache line size in order to avoid false sharing
262 : // read_index_cache_ and write_index_cache_ is used to reduce the amount of
263 : // cache coherency traffic
264 0 : alignas(cache_line_size_) std::atomic<size_t> write_index_{0};
265 0 : alignas(cache_line_size_) size_t read_index_cache_{0};
266 0 : alignas(cache_line_size_) std::atomic<size_t> read_index_{0};
267 0 : alignas(cache_line_size_) size_t write_index_cache_{0};
268 :
269 : // Padding to avoid adjacent allocations from sharing a cache line with
270 : // write_index_cache_
271 : // NOLINTNEXTLINE(modernize-avoid-c-arrays)
272 0 : char padding_data_[cache_line_size_ - sizeof(write_index_cache_)]{};
273 : };
274 : } // namespace Parallel
|