Jamba C++ API 8.0.0
Loading...
Searching...
No Matches
Concurrent.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2018-2019 pongasoft
3 *
4 * Licensed under the Apache License, Version 2.0 or the MIT license,
5 * at your option. You may not use this file except in compliance with
6 * one of these licenses. You may obtain copies of the licenses at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 * https://opensource.org/licenses/MIT
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 * License for the specific language governing permissions and limitations under
15 * the License.
16 *
17 * @author Yan Pujante
18 */
19#ifndef __PONGASOFT_UTILS_CONCURRENT_CONCURRENT_H__
20#define __PONGASOFT_UTILS_CONCURRENT_CONCURRENT_H__
21
22#include "SpinLock.h"
23
24#include <memory>
25
26namespace pongasoft {
27namespace Utils {
28
30template<typename M, typename T>
31concept ElementModifier = std::invocable<M, T*>;
32
35template<typename M, typename T>
37 std::invocable<M, T*> &&
38 std::convertible_to<std::invoke_result_t<M, T*>, bool>;
39
40namespace Concurrent {
41
42/*
43 * One of the big issues in the VST world comes from the fact that the processing thread maintains the state, but it
44 * is being accessed/changed by the UI thread (AudioEffect::setState and AudioEffect::getState). Moreover, if the
45 * processing thread needs to send a message to the UI thread, this must happen in a timer (running in the UI thread),
46 * meaning the processing thread needs to have a way to communicate the content of the message to the timer in a
47 * thread-safe way. See thread https://sdk.steinberg.net/viewtopic.php?f=4&t=516 for discussion.
48 *
49 * The 2 primitives required are an atomic value (for getState) and a queue with one element (where the element in the
50 * queue can be updated if not popped yet) (for setState / timer message)
51 *
52 * The golden rules of real time audio programming are not to use locks or memory allocation. Here are 2
53 * implementations with different tradeoffs.
54 *
55 * The LockFree namespace implements a version that does not use locks or allocate memory at runtime (only when
56 * the classes are created). The tradeoff is that it uses more memory (3 instances of T) and it is only thread
57 * safe when there is a single thread calling 'get' (resp 'pop') and another single thread calling 'set' (rep 'push').
58 *
59 * The WithSpinLock namespace a version which uses a very lightweight lock: a user space spin lock. The SpinLock
60 * implementation is not allocating any memory in any thread and is relying on the std::atomic_flag concept which is
61 * guaranteed to be lock free. It also does not make any system calls. The tradeoff is that the queue and atomic
62 * value do lock for the duration of the copy of T. The advantages are less memory use and fully multi thread safe.
63 */
64
65//------------------------------------------------------------------------
66// Lock Free Implementation of AtomicValue and SingleQueueElement
67//------------------------------------------------------------------------
68namespace LockFree {
77template<typename T>
79{
80public:
81 // wraps a unique pointer of type T and whether it is a new value or not
82 struct Element
83 {
84 Element(std::unique_ptr<T> iElement, bool iNew) noexcept : fElement{std::move(iElement)}, fNew{iNew} {}
85
86 std::unique_ptr<T> fElement;
87 bool fNew;
88 };
89
90public:
91 // Constructor
92 SingleElementStorage(std::unique_ptr<T> iElement, bool iIsEmpty) noexcept :
93 fSingleElement{new Element(std::move(iElement), !iIsEmpty)}
94 {}
95
96 // Destructor - Deletes the element created in the constructor
98 {
99 delete fSingleElement.exchange(nullptr);
100 }
101
102 // isEmpty
103 bool isEmpty() const
104 {
105 return !fSingleElement.load()->fNew;
106 }
107
112 bool __isLockFree() const { return fSingleElement.is_lock_free(); }
113
114protected:
119 std::unique_ptr<Element> store(std::unique_ptr<Element> iElement)
120 {
121 iElement->fNew = true;
122 iElement.reset(fSingleElement.exchange(iElement.release()));
123 return std::move(iElement);
124 }
125
137 std::unique_ptr<Element> load(std::unique_ptr<Element> iElement)
138 {
139 iElement->fNew = false;
140 iElement.reset(fSingleElement.exchange(iElement.release()));
141 return std::move(iElement);
142 }
143
144 // __newT => create a new T by using copy constructor
145 std::unique_ptr<T> __newT() const { return std::make_unique<T>(*(fSingleElement.load()->fElement)); }
146
147 // __newElement
148 std::unique_ptr<Element> __newElement() const { return std::make_unique<Element>(std::move(__newT()), false); }
149
150private:
151 // using a std::atomic on a pointer which should be lock free (check __isLockFree for sanity check!)
152 std::atomic<Element *> fSingleElement;
153};
154
160template<typename T>
162{
163public:
164 // Constructor
166 SingleElementStorage<T>{std::make_unique<T>(), true},
169 {}
170
173 explicit SingleElementQueue(std::unique_ptr<T> iElement, bool iIsEmpty = false) :
174 SingleElementStorage<T>{std::move(iElement), iIsEmpty},
177 {
178 }
179
180 //------------------------------------------------------------------------------------------------------------
181 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
182 //
183 // All the following methods (pop and last) should be called in a single thread
184 //
185 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
186 //------------------------------------------------------------------------------------------------------------
190 T *pop()
191 {
192 if(!this->isEmpty())
193 {
194 fPopValue = std::move(SingleElementStorage<T>::load(std::move(fPopValue)));
195 }
196
197 if(fPopValue->fNew)
198 {
199 fPopValue->fNew = false;
200 return fPopValue->fElement.get();
201 }
202
203 return nullptr;
204 }
205
209 bool pop(T &oElement)
210 {
211 auto element = pop();
212 if(element)
213 {
214 oElement = *element;
215 return true;
216 }
217
218 return false;
219 }
220
224 T const *last() const
225 {
226 return fPopValue->fElement.get();
227 }
228
229
233 void last(T &oElement) const
234 {
235 oElement = *last();
236 }
237
241 T const *popOrLast()
242 {
243 auto element = pop();
244
245 if(element)
246 return element;
247 else
248 return fPopValue->fElement.get();
249 }
250
251
255 void popOrLast(T &oElement)
256 {
257 oElement = *popOrLast();
258 }
259
260 //------------------------------------------------------------------------------------------------------------
261 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
262 //
263 // All the following methods (push and updateAndPush) should be called in a single thread
264 //
265 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
266 //------------------------------------------------------------------------------------------------------------
267
271 void push(T const &iElement)
272 {
273 *(fPushValue->fElement.get()) = iElement;
274 pushValue();
275 }
276
280 void push(T const *iElement)
281 {
282 *(fPushValue->fElement.get()) = *iElement;
283 pushValue();
284 }
285
290 template<ElementModifier<T> Modifier>
291 void updateAndPush(Modifier const &iElementModifier)
292 {
293 std::invoke(iElementModifier, fPushValue->fElement.get());
294 pushValue();
295 }
296
301 template<ElementPredicate<T> Modifier>
302 bool updateAndPushIf(Modifier const &iElementModifier)
303 {
304 if(std::invoke(iElementModifier, fPushValue->fElement.get()))
305 {
306 pushValue();
307 return true;
308 }
309 return false;
310 }
311private:
313 {
315 }
316
317private:
319
320 std::unique_ptr<Element> fPopValue;
321 std::unique_ptr<Element> fPushValue;
322};
323
329template<typename T>
331{
332public:
333 // Constructor - needs a value for initalizing the object
334 explicit AtomicValue(std::unique_ptr<T> iValue) :
335 SingleElementStorage<T>{std::move(iValue), false},
338 {}
339
340 //------------------------------------------------------------------------------------------------------------
341 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
342 //
343 // All the following methods (get) should be called in a single thread
344 //
345 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
346 //------------------------------------------------------------------------------------------------------------
347
351 T const *get()
352 {
353 if(!this->isEmpty())
354 {
355 fGetValue = std::move(SingleElementStorage<T>::load(std::move(fGetValue)));
356 }
357
358 return fGetValue->fElement.get();
359 }
360
365 {
366 return *get();
367 }
368
372 void get(T &oElement)
373 {
374 oElement = *get();
375 }
376
380 void get(T *oElement)
381 {
382 *oElement = *get();
383 }
384
385 //------------------------------------------------------------------------------------------------------------
386 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
387 //
388 // All the following methods (set / update) should be called in a single thread
389 //
390 // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
391 //------------------------------------------------------------------------------------------------------------
392
396 void set(T const &iValue)
397 {
398 *(fSetValue->fElement.get()) = iValue;
399 fSetValue = std::move(SingleElementStorage<T>::store(std::move(fSetValue)));
400 }
401
405 void set(T const *iValue)
406 {
407 *(fSetValue->fElement.get()) = *iValue;
408 fSetValue = std::move(SingleElementStorage<T>::store(std::move(fSetValue)));
409 }
410
415 template<ElementModifier<T> Modifier>
416 void update(Modifier const &iElementModifier)
417 {
418 std::invoke(iElementModifier, fSetValue->fElement.get());
419 fSetValue = std::move(SingleElementStorage<T>::store(std::move(fSetValue)));
420 }
421
426 template<ElementPredicate<T> Modifier>
427 bool updateIf(Modifier const &iElementModifier)
428 {
429 if(std::invoke(iElementModifier, fSetValue->fElement.get()))
430 {
431 fSetValue = std::move(SingleElementStorage<T>::store(std::move(fSetValue)));
432 return true;
433 }
434
435 return false;
436 }
437
438private:
440
441 std::unique_ptr<Element> fGetValue;
442 std::unique_ptr<Element> fSetValue;
443};
444}
445
449namespace WithSpinLock {
450
457template<typename T>
459{
460public:
461 SingleElementQueue() : fSingleElement{std::make_unique<T>()}, fIsEmpty{true}, fSpinLock{}
462 {}
463
468 explicit SingleElementQueue(std::unique_ptr<T> iFirstElement,
469 bool iIsEmpty = false) :
470 fSingleElement{std::move(iFirstElement)},
471 fIsEmpty{iIsEmpty},
472 fSpinLock{}
473 {}
474
481 bool isEmpty() const
482 {
483 auto lock = fSpinLock.acquire();
484 return fIsEmpty;
485 }
486
494 bool pop(T &oElement)
495 {
496 auto lock = fSpinLock.acquire();
497 if(fIsEmpty)
498 return false;
499
500 oElement = *fSingleElement;
501 fIsEmpty = true;
502
503 return true;
504 }
505
513 bool pop(T *oElement)
514 {
515 auto lock = fSpinLock.acquire();
516 if(fIsEmpty)
517 return false;
518
519 *oElement = *fSingleElement;
520 fIsEmpty = true;
521
522 return true;
523 }
524
525
530 void push(T const &iElement)
531 {
532 auto lock = fSpinLock.acquire();
533 *fSingleElement = iElement;
534 fIsEmpty = false;
535 }
536
541 void push(T const *iElement)
542 {
543 auto lock = fSpinLock.acquire();
544 *fSingleElement = *iElement;
545 fIsEmpty = false;
546 }
547
548private:
549 std::unique_ptr<T> fSingleElement;
552};
553
559template<typename T>
561{
562public:
563 explicit AtomicValue(std::unique_ptr<T> iValue) : fValue{std::move(iValue)}, fSpinLock{} {}
564
565 explicit AtomicValue(T const &iValue) : fValue{std::make_unique<T>(iValue)}, fSpinLock{} {}
566
571 T get()
572 {
573 auto lock = fSpinLock.acquire();
574 return *fValue;
575 }
576
581 void get(T &oElement)
582 {
583 auto lock = fSpinLock.acquire();
584 oElement = *fValue;
585 }
586
591 void get(T *oElement)
592 {
593 auto lock = fSpinLock.acquire();
594 *oElement = *fValue;
595 }
596
600 void set(T const &iValue)
601 {
602 auto lock = fSpinLock.acquire();
603 *fValue = iValue;
604 }
605
609 void set(T const *iValue)
610 {
611 auto lock = fSpinLock.acquire();
612 *fValue = *iValue;
613 }
614
615private:
616 std::unique_ptr<T> fValue;
618};
619
620
621}
622}
623}
624}
625
626#endif // __PONGASOFT_UTILS_CONCURRENT_CONCURRENT_H__
A simple implementation of a spin lock using the std::atomic_flag which is guaranteed to be atomic an...
Definition SpinLock.h:34
AtomicValue(std::unique_ptr< T > iValue)
Definition Concurrent.h:334
void update(Modifier const &iElementModifier)
Use this flavor to avoid copy.
Definition Concurrent.h:416
void set(T const *iValue)
Copy the value to make it accessible to get.
Definition Concurrent.h:405
SingleElementStorage< T >::Element Element
Definition Concurrent.h:439
void get(T *oElement)
Copy the value to *oElement.
Definition Concurrent.h:380
T const * get()
Definition Concurrent.h:351
void get(T &oElement)
Copy the value to oElement.
Definition Concurrent.h:372
std::unique_ptr< Element > fGetValue
Definition Concurrent.h:441
bool updateIf(Modifier const &iElementModifier)
Use this flavor to avoid copy.
Definition Concurrent.h:427
std::unique_ptr< Element > fSetValue
Definition Concurrent.h:442
void set(T const &iValue)
Copy the value to make it accessible to get.
Definition Concurrent.h:396
bool pop(T &oElement)
Copy the popped value to oElement and return true when there is a new value otherwise do nothing and ...
Definition Concurrent.h:209
void last(T &oElement) const
Copy the last value that was popped to oElement.
Definition Concurrent.h:233
T const * popOrLast()
Definition Concurrent.h:241
SingleElementStorage< T >::Element Element
Definition Concurrent.h:318
bool updateAndPushIf(Modifier const &iElementModifier)
Use this flavor of push to avoid copy.
Definition Concurrent.h:302
void push(T const &iElement)
Pushes (a copy of) iElement in the queue.
Definition Concurrent.h:271
SingleElementQueue(std::unique_ptr< T > iElement, bool iIsEmpty=false)
This constructor should be used if T does not provide an empty constructor.
Definition Concurrent.h:173
std::unique_ptr< Element > fPopValue
Definition Concurrent.h:320
T const * last() const
Definition Concurrent.h:224
std::unique_ptr< Element > fPushValue
Definition Concurrent.h:321
void push(T const *iElement)
Pushes (a copy of) *iElement in the queue.
Definition Concurrent.h:280
void updateAndPush(Modifier const &iElementModifier)
Use this flavor of push to avoid copy.
Definition Concurrent.h:291
void popOrLast(T &oElement)
Copy either the new value (if there is one) or the last value that was popped to oElement.
Definition Concurrent.h:255
SingleElementStorage(std::unique_ptr< T > iElement, bool iIsEmpty) noexcept
Definition Concurrent.h:92
std::unique_ptr< Element > load(std::unique_ptr< Element > iElement)
Loads an element from storage.
Definition Concurrent.h:137
bool __isLockFree() const
Used (from test) to make sure that it is a lock free implementation.
Definition Concurrent.h:112
std::unique_ptr< Element > store(std::unique_ptr< Element > iElement)
Stores an element in the storage.
Definition Concurrent.h:119
bool isEmpty() const
Definition Concurrent.h:103
std::unique_ptr< T > __newT() const
Definition Concurrent.h:145
std::unique_ptr< Element > __newElement() const
Definition Concurrent.h:148
std::atomic< Element * > fSingleElement
Definition Concurrent.h:152
SpinLock fSpinLock
Definition Concurrent.h:617
AtomicValue(std::unique_ptr< T > iValue)
Definition Concurrent.h:563
void set(T const *iValue)
Updates the current value with the provided one.
Definition Concurrent.h:609
std::unique_ptr< T > fValue
Definition Concurrent.h:616
void get(T *oElement)
Returns the "current" value.
Definition Concurrent.h:591
void get(T &oElement)
Returns the "current" value.
Definition Concurrent.h:581
AtomicValue(T const &iValue)
Definition Concurrent.h:565
void set(T const &iValue)
Updates the current value with the provided one.
Definition Concurrent.h:600
T get()
Returns the "current" value.
Definition Concurrent.h:571
bool pop(T &oElement)
Returns the single element in the queue if there is one.
Definition Concurrent.h:494
std::unique_ptr< T > fSingleElement
Definition Concurrent.h:549
void push(T const &iElement)
Pushes one element in the queue.
Definition Concurrent.h:530
bool pop(T *oElement)
Returns the single element in the queue if there is one.
Definition Concurrent.h:513
void push(T const *iElement)
Pushes one element in the queue.
Definition Concurrent.h:541
SingleElementQueue(std::unique_ptr< T > iFirstElement, bool iIsEmpty=false)
This constructor can be used to add one element to the queue right away or when there is no empty con...
Definition Concurrent.h:468
bool isEmpty() const
Note that although this api is thread safe, it will only report the state of the queue at the moment ...
Definition Concurrent.h:481
Concept for updating an element. We do not explicitly restrict the return type which is unused.
Definition Concurrent.h:31
Concept for updating an element.
Definition Concurrent.h:36
Definition Concurrent.h:68
The purpose of this namespace is to emphasize the fact that the implementation is using a spinlock.
Definition Concurrent.h:449
Definition Concurrent.h:40
Definition CircularBuffer.h:27
Definition Clock.h:23
Element(std::unique_ptr< T > iElement, bool iNew) noexcept
Definition Concurrent.h:84
std::unique_ptr< T > fElement
Definition Concurrent.h:86