Foundation
Loading...
Searching...
No Matches
AtomicQueue.hpp
Go to the documentation of this file.
1#pragma once
2#include <bit>
3#include "Allocator.hpp"
4#include "Atomic.hpp"
5#include "Container.hpp"
6#include "Logging.hpp"
7namespace Foundation::Core
8{
13 template <typename T>
15 {
16 const size_t mModulo;
19 // Only used in reader thread
20 size_t mReadCached{};
21 // Only used in writer thread
22 size_t mWriteCached{};
23
24 public:
30 SPSCQueue(size_t size, Allocator* alloc) : mModulo(size - 1), mBuffer(size, alloc)
31 {
32 CHECK_MSG((size & mModulo) == 0, "Size must be a power of two");
33 }
40 template <typename U>
41 bool Push(U&& data)
42 {
43 size_t write = mWrite.load(std::memory_order_relaxed);
44 // Amortize atomic reads
45 // We don't need to update the read head everytime. Only when we think we're full.
46 if (write - mReadCached == mBuffer.size()) [[unlikely]]
47 {
48 // Wrapped. Next cycle could begin only when there's enough space
49 mReadCached = mRead.load(std::memory_order_acquire);
50 if (write - mReadCached == mBuffer.size())
51 return false; // full
52 }
53 mBuffer[write & mModulo] = std::forward<U>(data);
54 mWrite.store(write + 1, std::memory_order_release);
55 return true;
56 }
64 bool Pop(T& out)
65 {
66 size_t read = mRead.load(std::memory_order_relaxed);
67 // Same as above
68 if (read == mWriteCached) [[unlikely]]
69 {
70 mWriteCached = mWrite.load(std::memory_order_acquire);
71 if (read == mWriteCached)
72 return false; // empty
73 }
74 mRead.store(read + 1, std::memory_order_release);
75 out = std::move(mBuffer[read & mModulo]);
76 return true;
77 }
78 };
83 template <typename T>
85 {
86 struct Data
87 {
88 T data{};
89 Atomic<size_t> writeCycle{} /* when to write */, readCycle{} /* when to read */;
90 };
91 const size_t mModulo, mShift;
94 size_t mWriteCached{};
95
96 public:
97 MPMCQueue(size_t size, Allocator* alloc) :
98 mModulo(size - 1), mShift(std::countr_zero(size)), mBuffer(size, alloc)
99 {
100 CHECK_MSG((size & mModulo) == 0, "Size must be a power of two");
101 }
102 template <typename U>
103 bool Push(U&& data)
104 {
105 size_t write = mWrite.load(std::memory_order_relaxed);
106 while (true)
107 {
108 auto& elem = mBuffer[write & mModulo];
109 size_t cycle = write >> mShift;
110 size_t read_cycle = elem.readCycle.load(std::memory_order_acquire);
111 // Waits for consumer to pop, if any
112 if (read_cycle == cycle) [[likely]] // Ready to write
113 {
114 // Bump the write index if we can, claiming the old index. Try later otherwise.
115 if (mWrite.compare_exchange_weak(write, write + 1, std::memory_order_relaxed))
116 {
117 elem.data = std::forward<U>(data);
118 elem.writeCycle.store(cycle + 1, std::memory_order_release);
119 return true;
120 }
121 }
122 else if (read_cycle >= cycle) // Not our turn yet? So we must be an old write. Update and try again. CAS
123 // does this already.
124 {
125 write = mWrite.load(std::memory_order_relaxed);
126 }
127 else // Counter is behind write cycle! We're full
128 {
129 return false;
130 }
131 }
132 }
133 bool Pop(T& out)
134 {
135 size_t read = mRead.load(std::memory_order_relaxed);
136 while (true)
137 {
138 // Same as above, but wait for producer
139 auto& elem = mBuffer[read & mModulo];
140 size_t cycle = read >> mShift;
141 size_t write_cycle = elem.writeCycle.load(std::memory_order_acquire);
142 if (write_cycle == cycle + 1)
143 {
144 // Same as above for reading
145 if (mRead.compare_exchange_weak(read, read + 1, std::memory_order_relaxed))
146 {
147 out = std::move(elem.data);
148 elem.readCycle.store(cycle + 1, std::memory_order_release);
149 return true;
150 }
151 }
152 else if (write_cycle > cycle) // Old read, not synced so try again.
153 {
154 read = mRead.load(std::memory_order_relaxed);
155 }
156 else // Read cycle is behind write cycle! We're empty
157 {
158 return false;
159 }
160 }
161 }
162 };
163} // namespace Foundation::Core
#define CHECK_MSG(expr, format_str,...)
Allocator interface (noexcept)
Definition Allocator.hpp:29
Atomic, bounded multi-producer multi-consumer FIFO ring buffer with a fixed maximum size.
Definition AtomicQueue.hpp:85
Vector< Data > mBuffer
Definition AtomicQueue.hpp:92
size_t mWriteCached
Definition AtomicQueue.hpp:94
MPMCQueue(size_t size, Allocator *alloc)
Definition AtomicQueue.hpp:97
Atomic< size_t > mRead
Definition AtomicQueue.hpp:93
bool Pop(T &out)
Definition AtomicQueue.hpp:133
const size_t mShift
Definition AtomicQueue.hpp:91
const size_t mModulo
Definition AtomicQueue.hpp:91
Atomic< size_t > mWrite
Definition AtomicQueue.hpp:93
bool Push(U &&data)
Definition AtomicQueue.hpp:103
Atomic, bounded single-producer single-consumer FIFO ring buffer with a fixed maximum size.
Definition AtomicQueue.hpp:15
size_t mReadCached
Definition AtomicQueue.hpp:20
const size_t mModulo
Definition AtomicQueue.hpp:16
bool Pop(T &out)
Try to pop data from the queue.
Definition AtomicQueue.hpp:64
Atomic< size_t > mWrite
Definition AtomicQueue.hpp:18
Atomic< size_t > mRead
Definition AtomicQueue.hpp:18
bool Push(U &&data)
Try to push data into the queue.
Definition AtomicQueue.hpp:41
size_t mWriteCached
Definition AtomicQueue.hpp:22
SPSCQueue(size_t size, Allocator *alloc)
Construct the SPSC Queue.
Definition AtomicQueue.hpp:30
Vector< T > mBuffer
Definition AtomicQueue.hpp:17
Lock-free atomic primitives and implementations of data structures.
Definition Allocator.hpp:6
std::vector< T, StlAllocator< T > > Vector
std::vector with explicit Foundation::Core::StlAllocator constructor
Definition Container.hpp:149
std::atomic< T > Atomic
Alias of std::atomic<T>.
Definition Atomic.hpp:26
Definition AtomicQueue.hpp:87
Atomic< size_t > writeCycle
Definition AtomicQueue.hpp:89
T data
Definition AtomicQueue.hpp:88
Atomic< size_t > readCycle
Definition AtomicQueue.hpp:89