libASPL
Loading...
Searching...
No Matches
DoubleBuffer.hpp
Go to the documentation of this file.
1// Copyright (c) libASPL authors
2// Licensed under MIT
3
4//! @file aspl/DoubleBuffer.hpp
5//! @brief Double buffer.
6
7#pragma once
8
9#include <CoreFoundation/CoreFoundation.h>
10
11#include <atomic>
12#include <mutex>
13#include <optional>
14#include <shared_mutex>
15#include <utility>
16
17namespace aspl {
18
19//! Doubly-buffered value with non-blocking read and blocking write.
20//!
21//! Logically, this is a single-value container, which has getter and setter
22//! with the following characteristics:
23//!
24//! - getters are running concurrently, and setters are serialized
25//!
26//! - setter is blocking; it may be blocked by both getters and setters,
27//! but in the average case only by setters
28//!
29//! - getter is non-blocking and lock-free; it does not block if a setter
30//! thread is suspended in the middle
31//!
32//! - getter is not wait-free though; if setters are called too frequently
33//! and have higher priority, then getter may spin until it has a chance
34//! to cut in between
35//!
36//! - sequential consistency is provided; after a setter returns, it's
37//! guaranteed that subsequent getters will observe up-to-date value
38//!
39//! Physically container is implemented as two copies of the value:
40//!
41//! - one copy is read-only and used by getters (concurrently)
42//!
43//! - another one is for setters; basically, the setter updates its copy
44//! while it's not visible to getters, waits until all getter using
45//! another copy finish, and switches the copies
46//!
47//! The algorithm is optimized for the following use case:
48//!
49//! - getters are frequent and setters are infrequent
50//!
51//! - the value is not very large and it's acceptable to make
52//! extra copies
53//!
54//! The value stored in the double buffer is immutable after it's set. To
55//! change the value, you need to call the getter, make a copy, modify it,
56//! and pass it to the setter.
57//!
58//! The value should have public default and copy constructors. If it also
59//! has move constructor, it can be used in setter.
60//!
61//! If the value has non-trivial destructor, before setter returns, it waits
62//! until the previously used value is not accessed by readers anymore and
63//! invokes destructor for the old value.
64//!
65//! Typical reader looks like the following:
66//! @code
67//! DoubleBuffer<T> fooBuf;
68//! ...
69//! {
70//! auto readLock = fooBuf.GetReadLock();
71//!
72//! const T& fooValue = readLock.GetReference();
73//! // safe read-only access to fooValue until block end
74//! }
75//! @endcode
76//!
77//! Or, if the reader is okay to call the copy constructor:
78//! @code
79//! DoubleBuffer<T> fooBuf;
80//! ...
81//! T fooValue = fooBuf.Get();
82//! @endcode
83//!
84//! Typical writer looks like this:
85//! @code
86//! DoubleBuffer<T> fooBuf;
87//! ...
88//! T fooValue = fooBuf.Get();
89//! // modify fooValue
90//! fooBuf.Set(std::move(fooValue));
91//! @endcode
92//!
93//! @see ReadLock.
94template <typename T>
96{
97 using BufferIndex = SInt64;
98
99 struct Buffer
100 {
101 std::optional<T> value = {};
102 std::atomic<BufferIndex> index = -1;
103 mutable std::shared_mutex mutex;
104 };
105
106public:
107 //! Read lock.
108 //!
109 //! Similar to scoped locks like std::lock_guard, but also provides
110 //! GetReference() method to access the value.
111 //!
112 //! While the lock is alive, it's safe to access the data for reading.
114 {
115 public:
116 //! Acquire the lock.
117 //! Non-blocking and lock-free.
118 //! May spin for a while if a setter is running concurrently, but wont
119 //! block if the setter is suspended somewhere on the halfway.
120 ReadLock(const DoubleBuffer& doubleBuffer)
121 {
122 for (;;) {
123 // Load current buffer index.
124 const auto currentIndex = doubleBuffer.currentIndex_.load();
125
126 // Get reference to the current buffer.
127 auto& currentBuffer = doubleBuffer.GetBufferAt(currentIndex);
128
129 // Try to lock buffer for reading.
130 //
131 // The lock can fail in the case when, after we loaded the current
132 // index, but before obtained the lock, the setter was called and
133 // finished once, and then was called another time and is currently
134 // in progress. Here we should retry and re-read current index.
135 //
136 // We're still lock-free because even if the ongoing setter is
137 // suspended, we wont block. Instead, on the next try we will
138 // switch to another buffer and will successfully obtain the lock.
139 //
140 // However, we're not wait-free because while this situation is
141 // repeating and new setters continue to come and preempt us, we'll
142 // have to retry. Fortunately, this is very unlikely to happen.
143 if (!currentBuffer.mutex.try_lock_shared()) {
144 continue;
145 }
146
147 // Check that the buffer index still matches the our current index
148 // after obtaining the lock.
149 //
150 // This check catches the case similar to the above one, but when
151 // the second setter is already finished.
152 if (currentBuffer.index.load() != currentIndex) {
153 currentBuffer.mutex.unlock_shared();
154 continue;
155 }
156
157 // At this point, it is guaranteed that any setter will see that
158 // we're using the buffer and wont rewrite it until we release
159 // the lock in destructor.
160 buffer_ = &currentBuffer;
161 return;
162 }
163 }
164
165 //! Release the lock.
167 {
168 if (buffer_) {
169 buffer_->mutex.unlock_shared();
170 }
171 }
172
173 //! Move lock.
175 : buffer_(other.buffer_)
176 {
177 other.buffer_ = nullptr;
178 }
179
180 ReadLock(const ReadLock&) = delete;
181 ReadLock& operator=(const ReadLock&) = delete;
182 ReadLock& operator=(ReadLock&&) = delete;
183
184 //! Get read-only reference to the value.
185 //! The reference may be used until read lock destructor is called.
186 //! The referred value is guaranteed to be immutable until that.
187 const T& GetReference() const
188 {
189 return *buffer_->value;
190 }
191
192 private:
193 const Buffer* buffer_ = nullptr;
194 };
195
196 //! Initialize buffer with given value.
197 explicit DoubleBuffer(const T& value)
198 {
199 Set(value);
200 }
201
202 //! Initialize buffer with given value.
203 explicit DoubleBuffer(T&& value = T())
204 {
205 Set(std::move(value));
206 }
207
208 ~DoubleBuffer() = default;
209
210 DoubleBuffer(const DoubleBuffer&) = delete;
211 DoubleBuffer& operator=(const DoubleBuffer&) = delete;
212
213 //! Get locked read-only reference to the value.
214 //! Non-blocking and lock-free, see ReadLock.
216 {
217 return ReadLock(*this);
218 }
219
220 //! Get copy of the value.
221 //! Non-blocking and lock-free, see ReadLock.
222 T Get() const
223 {
224 ReadLock readLock(*this);
225
226 return T(readLock.GetReference());
227 }
228
229 //! Set value.
230 //!
231 //! Blocks until all getters, invoked before this call, are finished.
232 //! Concurrent setter calls are serialized.
233 //!
234 //! It is guaranteed that if a getter is called after a setter returns,
235 //! the getter will observe the updated value.
236 //!
237 //! The value is either copied or moved into the internal buffer,
238 //! depending on whether an lvalue or rvalue is passed.
239 template <typename TT>
240 void Set(TT&& value)
241 {
242 // Serialize setters.
243 std::lock_guard writeLock(writeMutex_);
244
245 // Load current buffer index.
246 // Since this index is modified only by setters, it's safe to use "relaxed".
247 const auto oldIndex = currentIndex_.load(std::memory_order_relaxed);
248
249 // Calculate next index.
250 // Maximum signed value overflows to zero.
251 // Negative indices are reserved to indicate invalidated buffers.
252 const auto newIndex =
253 oldIndex < std::numeric_limits<BufferIndex>::max() ? oldIndex + 1 : 0;
254
255 // Get reference to the current buffer.
256 auto& oldBuffer = GetBufferAt(oldIndex);
257
258 // Get reference to the next buffer, i.e. not the current one.
259 auto& newBuffer = GetBufferAt(newIndex);
260
261 {
262 // Notify getters which were started earlier and are either working or going
263 // to work with this buffer that the buffer is going to be invalidated.
264 newBuffer.index = newIndex;
265
266 // Wait until finishing of ongoing getters that are still using this buffer.
267 //
268 // After we obtain the lock, we can be sure that old getters are either
269 // finished or will see the index which we've updated above and wont use the
270 // buffer.
271 //
272 // Note that since it is the other buffer, not the current one, chances are
273 // that there are no getters already, and most times we will acquire the lock
274 // immediately here.
275 std::unique_lock lock(newBuffer.mutex);
276
277 // Forward the value to the buffer.
278 newBuffer.value = std::forward<TT>(value);
279 }
280
281 // Switch current buffer index to the new buffer.
282 // It is guaranteed that all getters invoked after this point will return
283 // the new value.
284 currentIndex_ = newIndex;
285
286 if constexpr (!std::is_trivial<T>::value) {
287 // Notify getters which were started earlier and are either working or going
288 // to work with this buffer that the buffer is going to be invalidated.
289 oldBuffer.index = -1;
290
291 // Wait until finishing of ongoing getters that are still using the buffer.
292 // Here we can block for a while.
293 std::unique_lock lock(oldBuffer.mutex);
294
295 // Destroy value.
296 // This is needed to provide semantics of "replacing" value. We've written
297 // the new value and we should destroy the old one.
298 oldBuffer.value = {};
299 }
300 }
301
302private:
303 friend class ReadLock;
304
305 const Buffer& GetBufferAt(BufferIndex index) const
306 {
307 return buffers_[index % 2];
308 }
309
310 Buffer& GetBufferAt(BufferIndex index)
311 {
312 return buffers_[index % 2];
313 }
314
315 std::mutex writeMutex_;
316
317 Buffer buffers_[2];
318 std::atomic<BufferIndex> currentIndex_ = 0;
319};
320
321} // namespace aspl
ReadLock(ReadLock &&other)
Move lock.
const T & GetReference() const
Get read-only reference to the value. The reference may be used until read lock destructor is called....
ReadLock(const DoubleBuffer &doubleBuffer)
Acquire the lock. Non-blocking and lock-free. May spin for a while if a setter is running concurrentl...
Doubly-buffered value with non-blocking read and blocking write.
void Set(TT &&value)
Set value.
T Get() const
Get copy of the value. Non-blocking and lock-free, see ReadLock.
ReadLock GetReadLock() const
Get locked read-only reference to the value. Non-blocking and lock-free, see ReadLock.
DoubleBuffer(const T &value)
Initialize buffer with given value.
DoubleBuffer(T &&value=T())
Initialize buffer with given value.