Here is my lock-free queue implementation for single producer with some preallocated memory. T
is a simple type with no need for move operations. I don't use bool pop(T& result)
in order to avoid double checking. I would love to see your feedback!
template <typename T>
class LockFreeQueue {
public:
void setup(U8* buffer, U32 size) {
m_buffer = (T*)buffer;
m_size = size;
clear();
}
void clear() {
m_first.store(0, std::memory_order_relaxed);
m_last.store(0, std::memory_order_relaxed);
}
void push(const T& value) { // only for single producer
U32 last = m_last.load(std::memory_order_relaxed);
m_buffer[last] = value;
U32 next = inc(last);
ASSERT(next != m_first.load(std::memory_order_relaxed), "Queue overflow");
m_last.store(next, std::memory_order_release);
}
bool isEmpty() {
return m_first.load(std::memory_order_relaxed) == m_last.load(std::memory_order_relaxed);
}
void pop(T& result) { // for single consumer. check isEmpty() before
ASSERT(!isEmpty(), "Queue is empty");
U32 first = m_first.load(std::memory_order_relaxed);
result = m_buffer[first];
m_first.store(inc(first), std::memory_order_release);
}
bool tryPop(T& result) { // for multiple consumers. don't use isEmpty()
while (true) {
U32 first = m_first.load(std::memory_order_relaxed);
if (first == m_last.load(std::memory_order_relaxed))
return false;
result = m_buffer[first];
if (m_first.compare_exchange_weak(first, inc(first), std::memory_order_release))
return true;
}
}
private:
U32 inc(U32 n) { return (n + 1) % m_size; }
T* m_buffer;
U32 m_size;
std::atomic<U32> m_first;
std::atomic<U32> m_last;
};