Browse Source
[audio] replace ReaderWriterQueue with the generic SPSC queue (#2745)
[audio] replace ReaderWriterQueue with the generic SPSC queue (#2745)
A bit overkill for something that is only used once in the source code - should rather pertain to the generic SPSC queue just to avoid redundant code. If anything should be vendored. Signed-off-by: lizzie <lizzie@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2745 Reviewed-by: crueter <crueter@eden-emu.dev> Co-authored-by: lizzie <lizzie@eden-emu.dev> Co-committed-by: lizzie <lizzie@eden-emu.dev>pull/2748/head
committed by
crueter
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
5 changed files with 13 additions and 948 deletions
-
4src/audio_core/adsp/apps/audio_renderer/audio_renderer.h
-
9src/audio_core/sink/sink_stream.cpp
-
7src/audio_core/sink/sink_stream.h
-
1src/common/CMakeLists.txt
-
940src/common/reader_writer_queue.h
@ -1,940 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2013-2020 Cameron Desrochers |
|
||||
// SPDX-License-Identifier: BSD-2-Clause |
|
||||
|
|
||||
#pragma once |
|
||||
|
|
||||
#include <cassert> |
|
||||
#include <cstdint> |
|
||||
#include <cstdlib> // For malloc/free/abort & size_t |
|
||||
#include <memory> |
|
||||
#include <new> |
|
||||
#include <stdexcept> |
|
||||
#include <type_traits> |
|
||||
#include <utility> |
|
||||
|
|
||||
#include "common/atomic_helpers.h" |
|
||||
|
|
||||
#if __cplusplus > 199711L || _MSC_VER >= 1700 // C++11 or VS2012 |
|
||||
#include <chrono> |
|
||||
#endif |
|
||||
|
|
||||
// A lock-free queue for a single-consumer, single-producer architecture. |
|
||||
// The queue is also wait-free in the common path (except if more memory |
|
||||
// needs to be allocated, in which case malloc is called). |
|
||||
// Allocates memory sparingly, and only once if the original maximum size |
|
||||
// estimate is never exceeded. |
|
||||
// Tested on x86/x64 processors, but semantics should be correct for all |
|
||||
// architectures (given the right implementations in atomicops.h), provided |
|
||||
// that aligned integer and pointer accesses are naturally atomic. |
|
||||
// Note that there should only be one consumer thread and producer thread; |
|
||||
// Switching roles of the threads, or using multiple consecutive threads for |
|
||||
// one role, is not safe unless properly synchronized. |
|
||||
// Using the queue exclusively from one thread is fine, though a bit silly. |
|
||||
|
|
||||
#ifndef MOODYCAMEL_CACHE_LINE_SIZE |
|
||||
#define MOODYCAMEL_CACHE_LINE_SIZE 64 |
|
||||
#endif |
|
||||
|
|
||||
#ifndef MOODYCAMEL_EXCEPTIONS_ENABLED |
|
||||
#if (defined(_MSC_VER) && defined(_CPPUNWIND)) || (defined(__GNUC__) && defined(__EXCEPTIONS)) || \ |
|
||||
(!defined(_MSC_VER) && !defined(__GNUC__)) |
|
||||
#define MOODYCAMEL_EXCEPTIONS_ENABLED |
|
||||
#endif |
|
||||
#endif |
|
||||
|
|
||||
#ifndef MOODYCAMEL_HAS_EMPLACE |
|
||||
#if !defined(_MSC_VER) || \ |
|
||||
_MSC_VER >= 1800 // variadic templates: either a non-MS compiler or VS >= 2013 |
|
||||
#define MOODYCAMEL_HAS_EMPLACE 1 |
|
||||
#endif |
|
||||
#endif |
|
||||
|
|
||||
#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE |
|
||||
#if defined(__APPLE__) && defined(__MACH__) && __cplusplus >= 201703L |
|
||||
// This is required to find out what deployment target we are using |
|
||||
#include <CoreFoundation/CoreFoundation.h> |
|
||||
#if !defined(MAC_OS_X_VERSION_MIN_REQUIRED) || \ |
|
||||
MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 |
|
||||
// C++17 new(size_t, align_val_t) is not backwards-compatible with older versions of macOS, so we |
|
||||
// can't support over-alignment in this case |
|
||||
#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE |
|
||||
#endif |
|
||||
#endif |
|
||||
#endif |
|
||||
|
|
||||
#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE |
|
||||
#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE AE_ALIGN(MOODYCAMEL_CACHE_LINE_SIZE) |
|
||||
#endif |
|
||||
|
|
||||
#ifdef AE_VCPP |
|
||||
#pragma warning(push) |
|
||||
#pragma warning(disable : 4324) // structure was padded due to __declspec(align()) |
|
||||
#pragma warning(disable : 4820) // padding was added |
|
||||
#pragma warning(disable : 4127) // conditional expression is constant |
|
||||
#endif |
|
||||
|
|
||||
namespace Common { |
|
||||
|
|
||||
template <typename T, size_t MAX_BLOCK_SIZE = 512> |
|
||||
class MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE ReaderWriterQueue { |
|
||||
// Design: Based on a queue-of-queues. The low-level queues are just |
|
||||
// circular buffers with front and tail indices indicating where the |
|
||||
// next element to dequeue is and where the next element can be enqueued, |
|
||||
// respectively. Each low-level queue is called a "block". Each block |
|
||||
// wastes exactly one element's worth of space to keep the design simple |
|
||||
// (if front == tail then the queue is empty, and can't be full). |
|
||||
// The high-level queue is a circular linked list of blocks; again there |
|
||||
// is a front and tail, but this time they are pointers to the blocks. |
|
||||
// The front block is where the next element to be dequeued is, provided |
|
||||
// the block is not empty. The back block is where elements are to be |
|
||||
// enqueued, provided the block is not full. |
|
||||
// The producer thread owns all the tail indices/pointers. The consumer |
|
||||
// thread owns all the front indices/pointers. Both threads read each |
|
||||
// other's variables, but only the owning thread updates them. E.g. After |
|
||||
// the consumer reads the producer's tail, the tail may change before the |
|
||||
// consumer is done dequeuing an object, but the consumer knows the tail |
|
||||
// will never go backwards, only forwards. |
|
||||
// If there is no room to enqueue an object, an additional block (of |
|
||||
// equal size to the last block) is added. Blocks are never removed. |
|
||||
|
|
||||
public: |
|
||||
typedef T value_type; |
|
||||
|
|
||||
// Constructs a queue that can hold at least `size` elements without further |
|
||||
// allocations. If more than MAX_BLOCK_SIZE elements are requested, |
|
||||
// then several blocks of MAX_BLOCK_SIZE each are reserved (including |
|
||||
// at least one extra buffer block). |
|
||||
AE_NO_TSAN explicit ReaderWriterQueue(size_t size = 15) |
|
||||
#ifndef NDEBUG |
|
||||
: enqueuing(false), dequeuing(false) |
|
||||
#endif |
|
||||
{ |
|
||||
assert(MAX_BLOCK_SIZE == ceilToPow2(MAX_BLOCK_SIZE) && |
|
||||
"MAX_BLOCK_SIZE must be a power of 2"); |
|
||||
assert(MAX_BLOCK_SIZE >= 2 && "MAX_BLOCK_SIZE must be at least 2"); |
|
||||
|
|
||||
Block* firstBlock = nullptr; |
|
||||
|
|
||||
largestBlockSize = |
|
||||
ceilToPow2(size + 1); // We need a spare slot to fit size elements in the block |
|
||||
if (largestBlockSize > MAX_BLOCK_SIZE * 2) { |
|
||||
// We need a spare block in case the producer is writing to a different block the |
|
||||
// consumer is reading from, and wants to enqueue the maximum number of elements. We |
|
||||
// also need a spare element in each block to avoid the ambiguity between front == tail |
|
||||
// meaning "empty" and "full". So the effective number of slots that are guaranteed to |
|
||||
// be usable at any time is the block size - 1 times the number of blocks - 1. Solving |
|
||||
// for size and applying a ceiling to the division gives us (after simplifying): |
|
||||
size_t initialBlockCount = (size + MAX_BLOCK_SIZE * 2 - 3) / (MAX_BLOCK_SIZE - 1); |
|
||||
largestBlockSize = MAX_BLOCK_SIZE; |
|
||||
Block* lastBlock = nullptr; |
|
||||
for (size_t i = 0; i != initialBlockCount; ++i) { |
|
||||
auto block = make_block(largestBlockSize); |
|
||||
if (block == nullptr) { |
|
||||
#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED |
|
||||
throw std::bad_alloc(); |
|
||||
#else |
|
||||
abort(); |
|
||||
#endif |
|
||||
} |
|
||||
if (firstBlock == nullptr) { |
|
||||
firstBlock = block; |
|
||||
} else { |
|
||||
lastBlock->next = block; |
|
||||
} |
|
||||
lastBlock = block; |
|
||||
block->next = firstBlock; |
|
||||
} |
|
||||
} else { |
|
||||
firstBlock = make_block(largestBlockSize); |
|
||||
if (firstBlock == nullptr) { |
|
||||
#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED |
|
||||
throw std::bad_alloc(); |
|
||||
#else |
|
||||
abort(); |
|
||||
#endif |
|
||||
} |
|
||||
firstBlock->next = firstBlock; |
|
||||
} |
|
||||
frontBlock = firstBlock; |
|
||||
tailBlock = firstBlock; |
|
||||
|
|
||||
// Make sure the reader/writer threads will have the initialized memory setup above: |
|
||||
fence(memory_order_sync); |
|
||||
} |
|
||||
|
|
||||
// Note: The queue should not be accessed concurrently while it's |
|
||||
// being moved. It's up to the user to synchronize this. |
|
||||
AE_NO_TSAN ReaderWriterQueue(ReaderWriterQueue&& other) |
|
||||
: frontBlock(other.frontBlock.load()), tailBlock(other.tailBlock.load()), |
|
||||
largestBlockSize(other.largestBlockSize) |
|
||||
#ifndef NDEBUG |
|
||||
, |
|
||||
enqueuing(false), dequeuing(false) |
|
||||
#endif |
|
||||
{ |
|
||||
other.largestBlockSize = 32; |
|
||||
Block* b = other.make_block(other.largestBlockSize); |
|
||||
if (b == nullptr) { |
|
||||
#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED |
|
||||
throw std::bad_alloc(); |
|
||||
#else |
|
||||
abort(); |
|
||||
#endif |
|
||||
} |
|
||||
b->next = b; |
|
||||
other.frontBlock = b; |
|
||||
other.tailBlock = b; |
|
||||
} |
|
||||
|
|
||||
// Note: The queue should not be accessed concurrently while it's |
|
||||
// being moved. It's up to the user to synchronize this. |
|
||||
ReaderWriterQueue& operator=(ReaderWriterQueue&& other) AE_NO_TSAN { |
|
||||
Block* b = frontBlock.load(); |
|
||||
frontBlock = other.frontBlock.load(); |
|
||||
other.frontBlock = b; |
|
||||
b = tailBlock.load(); |
|
||||
tailBlock = other.tailBlock.load(); |
|
||||
other.tailBlock = b; |
|
||||
std::swap(largestBlockSize, other.largestBlockSize); |
|
||||
return *this; |
|
||||
} |
|
||||
|
|
||||
// Note: The queue should not be accessed concurrently while it's |
|
||||
// being deleted. It's up to the user to synchronize this. |
|
||||
AE_NO_TSAN ~ReaderWriterQueue() { |
|
||||
// Make sure we get the latest version of all variables from other CPUs: |
|
||||
fence(memory_order_sync); |
|
||||
|
|
||||
// Destroy any remaining objects in queue and free memory |
|
||||
Block* frontBlock_ = frontBlock; |
|
||||
Block* block = frontBlock_; |
|
||||
do { |
|
||||
Block* nextBlock = block->next; |
|
||||
size_t blockFront = block->front; |
|
||||
size_t blockTail = block->tail; |
|
||||
|
|
||||
for (size_t i = blockFront; i != blockTail; i = (i + 1) & block->sizeMask) { |
|
||||
auto element = reinterpret_cast<T*>(block->data + i * sizeof(T)); |
|
||||
element->~T(); |
|
||||
(void)element; |
|
||||
} |
|
||||
|
|
||||
auto rawBlock = block->rawThis; |
|
||||
block->~Block(); |
|
||||
std::free(rawBlock); |
|
||||
block = nextBlock; |
|
||||
} while (block != frontBlock_); |
|
||||
} |
|
||||
|
|
||||
// Enqueues a copy of element if there is room in the queue. |
|
||||
// Returns true if the element was enqueued, false otherwise. |
|
||||
// Does not allocate memory. |
|
||||
AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN { |
|
||||
return inner_enqueue<CannotAlloc>(element); |
|
||||
} |
|
||||
|
|
||||
// Enqueues a moved copy of element if there is room in the queue. |
|
||||
// Returns true if the element was enqueued, false otherwise. |
|
||||
// Does not allocate memory. |
|
||||
AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN { |
|
||||
return inner_enqueue<CannotAlloc>(std::forward<T>(element)); |
|
||||
} |
|
||||
|
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
// Like try_enqueue() but with emplace semantics (i.e. construct-in-place). |
|
||||
template <typename... Args> |
|
||||
AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN { |
|
||||
return inner_enqueue<CannotAlloc>(std::forward<Args>(args)...); |
|
||||
} |
|
||||
#endif |
|
||||
|
|
||||
// Enqueues a copy of element on the queue. |
|
||||
// Allocates an additional block of memory if needed. |
|
||||
// Only fails (returns false) if memory allocation fails. |
|
||||
AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN { |
|
||||
return inner_enqueue<CanAlloc>(element); |
|
||||
} |
|
||||
|
|
||||
// Enqueues a moved copy of element on the queue. |
|
||||
// Allocates an additional block of memory if needed. |
|
||||
// Only fails (returns false) if memory allocation fails. |
|
||||
AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN { |
|
||||
return inner_enqueue<CanAlloc>(std::forward<T>(element)); |
|
||||
} |
|
||||
|
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
// Like enqueue() but with emplace semantics (i.e. construct-in-place). |
|
||||
template <typename... Args> |
|
||||
AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN { |
|
||||
return inner_enqueue<CanAlloc>(std::forward<Args>(args)...); |
|
||||
} |
|
||||
#endif |
|
||||
|
|
||||
// Attempts to dequeue an element; if the queue is empty, |
|
||||
// returns false instead. If the queue has at least one element, |
|
||||
// moves front to result using operator=, then returns true. |
|
||||
template <typename U> |
|
||||
bool try_dequeue(U& result) AE_NO_TSAN { |
|
||||
#ifndef NDEBUG |
|
||||
ReentrantGuard guard(this->dequeuing); |
|
||||
#endif |
|
||||
|
|
||||
// High-level pseudocode: |
|
||||
// Remember where the tail block is |
|
||||
// If the front block has an element in it, dequeue it |
|
||||
// Else |
|
||||
// If front block was the tail block when we entered the function, return false |
|
||||
// Else advance to next block and dequeue the item there |
|
||||
|
|
||||
// Note that we have to use the value of the tail block from before we check if the front |
|
||||
// block is full or not, in case the front block is empty and then, before we check if the |
|
||||
// tail block is at the front block or not, the producer fills up the front block *and |
|
||||
// moves on*, which would make us skip a filled block. Seems unlikely, but was consistently |
|
||||
// reproducible in practice. |
|
||||
// In order to avoid overhead in the common case, though, we do a double-checked pattern |
|
||||
// where we have the fast path if the front block is not empty, then read the tail block, |
|
||||
// then re-read the front block and check if it's not empty again, then check if the tail |
|
||||
// block has advanced. |
|
||||
|
|
||||
Block* frontBlock_ = frontBlock.load(); |
|
||||
size_t blockTail = frontBlock_->localTail; |
|
||||
size_t blockFront = frontBlock_->front.load(); |
|
||||
|
|
||||
if (blockFront != blockTail || |
|
||||
blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
non_empty_front_block: |
|
||||
// Front block not empty, dequeue from here |
|
||||
auto element = reinterpret_cast<T*>(frontBlock_->data + blockFront * sizeof(T)); |
|
||||
result = std::move(*element); |
|
||||
element->~T(); |
|
||||
|
|
||||
blockFront = (blockFront + 1) & frontBlock_->sizeMask; |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
frontBlock_->front = blockFront; |
|
||||
} else if (frontBlock_ != tailBlock.load()) { |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
frontBlock_ = frontBlock.load(); |
|
||||
blockTail = frontBlock_->localTail = frontBlock_->tail.load(); |
|
||||
blockFront = frontBlock_->front.load(); |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
if (blockFront != blockTail) { |
|
||||
// Oh look, the front block isn't empty after all |
|
||||
goto non_empty_front_block; |
|
||||
} |
|
||||
|
|
||||
// Front block is empty but there's another block ahead, advance to it |
|
||||
Block* nextBlock = frontBlock_->next; |
|
||||
// Don't need an acquire fence here since next can only ever be set on the tailBlock, |
|
||||
// and we're not the tailBlock, and we did an acquire earlier after reading tailBlock |
|
||||
// which ensures next is up-to-date on this CPU in case we recently were at tailBlock. |
|
||||
|
|
||||
size_t nextBlockFront = nextBlock->front.load(); |
|
||||
size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load(); |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
// Since the tailBlock is only ever advanced after being written to, |
|
||||
// we know there's for sure an element to dequeue on it |
|
||||
assert(nextBlockFront != nextBlockTail); |
|
||||
AE_UNUSED(nextBlockTail); |
|
||||
|
|
||||
// We're done with this block, let the producer use it if it needs |
|
||||
fence(memory_order_release); // Expose possibly pending changes to frontBlock->front |
|
||||
// from last dequeue |
|
||||
frontBlock = frontBlock_ = nextBlock; |
|
||||
|
|
||||
compiler_fence(memory_order_release); // Not strictly needed |
|
||||
|
|
||||
auto element = reinterpret_cast<T*>(frontBlock_->data + nextBlockFront * sizeof(T)); |
|
||||
|
|
||||
result = std::move(*element); |
|
||||
element->~T(); |
|
||||
|
|
||||
nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask; |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
frontBlock_->front = nextBlockFront; |
|
||||
} else { |
|
||||
// No elements in current block and no other block to advance to |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// Returns a pointer to the front element in the queue (the one that |
|
||||
// would be removed next by a call to `try_dequeue` or `pop`). If the |
|
||||
// queue appears empty at the time the method is called, nullptr is |
|
||||
// returned instead. |
|
||||
// Must be called only from the consumer thread. |
|
||||
T* peek() const AE_NO_TSAN { |
|
||||
#ifndef NDEBUG |
|
||||
ReentrantGuard guard(this->dequeuing); |
|
||||
#endif |
|
||||
// See try_dequeue() for reasoning |
|
||||
|
|
||||
Block* frontBlock_ = frontBlock.load(); |
|
||||
size_t blockTail = frontBlock_->localTail; |
|
||||
size_t blockFront = frontBlock_->front.load(); |
|
||||
|
|
||||
if (blockFront != blockTail || |
|
||||
blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { |
|
||||
fence(memory_order_acquire); |
|
||||
non_empty_front_block: |
|
||||
return reinterpret_cast<T*>(frontBlock_->data + blockFront * sizeof(T)); |
|
||||
} else if (frontBlock_ != tailBlock.load()) { |
|
||||
fence(memory_order_acquire); |
|
||||
frontBlock_ = frontBlock.load(); |
|
||||
blockTail = frontBlock_->localTail = frontBlock_->tail.load(); |
|
||||
blockFront = frontBlock_->front.load(); |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
if (blockFront != blockTail) { |
|
||||
goto non_empty_front_block; |
|
||||
} |
|
||||
|
|
||||
Block* nextBlock = frontBlock_->next; |
|
||||
|
|
||||
size_t nextBlockFront = nextBlock->front.load(); |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
assert(nextBlockFront != nextBlock->tail.load()); |
|
||||
return reinterpret_cast<T*>(nextBlock->data + nextBlockFront * sizeof(T)); |
|
||||
} |
|
||||
|
|
||||
return nullptr; |
|
||||
} |
|
||||
|
|
||||
// Removes the front element from the queue, if any, without returning it. |
|
||||
// Returns true on success, or false if the queue appeared empty at the time |
|
||||
// `pop` was called. |
|
||||
bool pop() AE_NO_TSAN { |
|
||||
#ifndef NDEBUG |
|
||||
ReentrantGuard guard(this->dequeuing); |
|
||||
#endif |
|
||||
// See try_dequeue() for reasoning |
|
||||
|
|
||||
Block* frontBlock_ = frontBlock.load(); |
|
||||
size_t blockTail = frontBlock_->localTail; |
|
||||
size_t blockFront = frontBlock_->front.load(); |
|
||||
|
|
||||
if (blockFront != blockTail || |
|
||||
blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) { |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
non_empty_front_block: |
|
||||
auto element = reinterpret_cast<T*>(frontBlock_->data + blockFront * sizeof(T)); |
|
||||
element->~T(); |
|
||||
|
|
||||
blockFront = (blockFront + 1) & frontBlock_->sizeMask; |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
frontBlock_->front = blockFront; |
|
||||
} else if (frontBlock_ != tailBlock.load()) { |
|
||||
fence(memory_order_acquire); |
|
||||
frontBlock_ = frontBlock.load(); |
|
||||
blockTail = frontBlock_->localTail = frontBlock_->tail.load(); |
|
||||
blockFront = frontBlock_->front.load(); |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
if (blockFront != blockTail) { |
|
||||
goto non_empty_front_block; |
|
||||
} |
|
||||
|
|
||||
// Front block is empty but there's another block ahead, advance to it |
|
||||
Block* nextBlock = frontBlock_->next; |
|
||||
|
|
||||
size_t nextBlockFront = nextBlock->front.load(); |
|
||||
size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load(); |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
assert(nextBlockFront != nextBlockTail); |
|
||||
AE_UNUSED(nextBlockTail); |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
frontBlock = frontBlock_ = nextBlock; |
|
||||
|
|
||||
compiler_fence(memory_order_release); |
|
||||
|
|
||||
auto element = reinterpret_cast<T*>(frontBlock_->data + nextBlockFront * sizeof(T)); |
|
||||
element->~T(); |
|
||||
|
|
||||
nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask; |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
frontBlock_->front = nextBlockFront; |
|
||||
} else { |
|
||||
// No elements in current block and no other block to advance to |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// Returns the approximate number of items currently in the queue. |
|
||||
// Safe to call from both the producer and consumer threads. |
|
||||
inline size_t size_approx() const AE_NO_TSAN { |
|
||||
size_t result = 0; |
|
||||
Block* frontBlock_ = frontBlock.load(); |
|
||||
Block* block = frontBlock_; |
|
||||
do { |
|
||||
fence(memory_order_acquire); |
|
||||
size_t blockFront = block->front.load(); |
|
||||
size_t blockTail = block->tail.load(); |
|
||||
result += (blockTail - blockFront) & block->sizeMask; |
|
||||
block = block->next.load(); |
|
||||
} while (block != frontBlock_); |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
// Returns the total number of items that could be enqueued without incurring |
|
||||
// an allocation when this queue is empty. |
|
||||
// Safe to call from both the producer and consumer threads. |
|
||||
// |
|
||||
// NOTE: The actual capacity during usage may be different depending on the consumer. |
|
||||
// If the consumer is removing elements concurrently, the producer cannot add to |
|
||||
// the block the consumer is removing from until it's completely empty, except in |
|
||||
// the case where the producer was writing to the same block the consumer was |
|
||||
// reading from the whole time. |
|
||||
inline size_t max_capacity() const { |
|
||||
size_t result = 0; |
|
||||
Block* frontBlock_ = frontBlock.load(); |
|
||||
Block* block = frontBlock_; |
|
||||
do { |
|
||||
fence(memory_order_acquire); |
|
||||
result += block->sizeMask; |
|
||||
block = block->next.load(); |
|
||||
} while (block != frontBlock_); |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
private: |
|
||||
enum AllocationMode { CanAlloc, CannotAlloc }; |
|
||||
|
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
template <AllocationMode canAlloc, typename... Args> |
|
||||
bool inner_enqueue(Args&&... args) AE_NO_TSAN |
|
||||
#else |
|
||||
template <AllocationMode canAlloc, typename U> |
|
||||
bool inner_enqueue(U&& element) AE_NO_TSAN |
|
||||
#endif |
|
||||
{ |
|
||||
#ifndef NDEBUG |
|
||||
ReentrantGuard guard(this->enqueuing); |
|
||||
#endif |
|
||||
|
|
||||
// High-level pseudocode (assuming we're allowed to alloc a new block): |
|
||||
// If room in tail block, add to tail |
|
||||
// Else check next block |
|
||||
// If next block is not the head block, enqueue on next block |
|
||||
// Else create a new block and enqueue there |
|
||||
// Advance tail to the block we just enqueued to |
|
||||
|
|
||||
Block* tailBlock_ = tailBlock.load(); |
|
||||
size_t blockFront = tailBlock_->localFront; |
|
||||
size_t blockTail = tailBlock_->tail.load(); |
|
||||
|
|
||||
size_t nextBlockTail = (blockTail + 1) & tailBlock_->sizeMask; |
|
||||
if (nextBlockTail != blockFront || |
|
||||
nextBlockTail != (tailBlock_->localFront = tailBlock_->front.load())) { |
|
||||
fence(memory_order_acquire); |
|
||||
// This block has room for at least one more element |
|
||||
char* location = tailBlock_->data + blockTail * sizeof(T); |
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
new (location) T(std::forward<Args>(args)...); |
|
||||
#else |
|
||||
new (location) T(std::forward<U>(element)); |
|
||||
#endif |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
tailBlock_->tail = nextBlockTail; |
|
||||
} else { |
|
||||
fence(memory_order_acquire); |
|
||||
if (tailBlock_->next.load() != frontBlock) { |
|
||||
// Note that the reason we can't advance to the frontBlock and start adding new |
|
||||
// entries there is because if we did, then dequeue would stay in that block, |
|
||||
// eventually reading the new values, instead of advancing to the next full block |
|
||||
// (whose values were enqueued first and so should be consumed first). |
|
||||
|
|
||||
fence(memory_order_acquire); // Ensure we get latest writes if we got the latest |
|
||||
// frontBlock |
|
||||
|
|
||||
// tailBlock is full, but there's a free block ahead, use it |
|
||||
Block* tailBlockNext = tailBlock_->next.load(); |
|
||||
size_t nextBlockFront = tailBlockNext->localFront = tailBlockNext->front.load(); |
|
||||
nextBlockTail = tailBlockNext->tail.load(); |
|
||||
fence(memory_order_acquire); |
|
||||
|
|
||||
// This block must be empty since it's not the head block and we |
|
||||
// go through the blocks in a circle |
|
||||
assert(nextBlockFront == nextBlockTail); |
|
||||
tailBlockNext->localFront = nextBlockFront; |
|
||||
|
|
||||
char* location = tailBlockNext->data + nextBlockTail * sizeof(T); |
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
new (location) T(std::forward<Args>(args)...); |
|
||||
#else |
|
||||
new (location) T(std::forward<U>(element)); |
|
||||
#endif |
|
||||
|
|
||||
tailBlockNext->tail = (nextBlockTail + 1) & tailBlockNext->sizeMask; |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
tailBlock = tailBlockNext; |
|
||||
} else if (canAlloc == CanAlloc) { |
|
||||
// tailBlock is full and there's no free block ahead; create a new block |
|
||||
auto newBlockSize = |
|
||||
largestBlockSize >= MAX_BLOCK_SIZE ? largestBlockSize : largestBlockSize * 2; |
|
||||
auto newBlock = make_block(newBlockSize); |
|
||||
if (newBlock == nullptr) { |
|
||||
// Could not allocate a block! |
|
||||
return false; |
|
||||
} |
|
||||
largestBlockSize = newBlockSize; |
|
||||
|
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
new (newBlock->data) T(std::forward<Args>(args)...); |
|
||||
#else |
|
||||
new (newBlock->data) T(std::forward<U>(element)); |
|
||||
#endif |
|
||||
assert(newBlock->front == 0); |
|
||||
newBlock->tail = newBlock->localTail = 1; |
|
||||
|
|
||||
newBlock->next = tailBlock_->next.load(); |
|
||||
tailBlock_->next = newBlock; |
|
||||
|
|
||||
// Might be possible for the dequeue thread to see the new tailBlock->next |
|
||||
// *without* seeing the new tailBlock value, but this is OK since it can't |
|
||||
// advance to the next block until tailBlock is set anyway (because the only |
|
||||
// case where it could try to read the next is if it's already at the tailBlock, |
|
||||
// and it won't advance past tailBlock in any circumstance). |
|
||||
|
|
||||
fence(memory_order_release); |
|
||||
tailBlock = newBlock; |
|
||||
} else if (canAlloc == CannotAlloc) { |
|
||||
// Would have had to allocate a new block to enqueue, but not allowed |
|
||||
return false; |
|
||||
} else { |
|
||||
assert(false && "Should be unreachable code"); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// Disable copying |
|
||||
ReaderWriterQueue(ReaderWriterQueue const&) {} |
|
||||
|
|
||||
// Disable assignment |
|
||||
ReaderWriterQueue& operator=(ReaderWriterQueue const&) {} |
|
||||
|
|
||||
AE_FORCEINLINE static size_t ceilToPow2(size_t x) { |
|
||||
// From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 |
|
||||
--x; |
|
||||
x |= x >> 1; |
|
||||
x |= x >> 2; |
|
||||
x |= x >> 4; |
|
||||
for (size_t i = 1; i < sizeof(size_t); i <<= 1) { |
|
||||
x |= x >> (i << 3); |
|
||||
} |
|
||||
++x; |
|
||||
return x; |
|
||||
} |
|
||||
|
|
||||
template <typename U> |
|
||||
static AE_FORCEINLINE char* align_for(char* ptr) AE_NO_TSAN { |
|
||||
const std::size_t alignment = std::alignment_of<U>::value; |
|
||||
return ptr + (alignment - (reinterpret_cast<std::uintptr_t>(ptr) % alignment)) % alignment; |
|
||||
} |
|
||||
|
|
||||
private: |
|
||||
#ifndef NDEBUG |
|
||||
struct ReentrantGuard { |
|
||||
AE_NO_TSAN ReentrantGuard(weak_atomic<bool>& _inSection) : inSection(_inSection) { |
|
||||
assert(!inSection && |
|
||||
"Concurrent (or re-entrant) enqueue or dequeue operation detected (only one " |
|
||||
"thread at a time may hold the producer or consumer role)"); |
|
||||
inSection = true; |
|
||||
} |
|
||||
|
|
||||
AE_NO_TSAN ~ReentrantGuard() { |
|
||||
inSection = false; |
|
||||
} |
|
||||
|
|
||||
private: |
|
||||
ReentrantGuard& operator=(ReentrantGuard const&); |
|
||||
|
|
||||
private: |
|
||||
weak_atomic<bool>& inSection; |
|
||||
}; |
|
||||
#endif |
|
||||
|
|
||||
struct Block { |
|
||||
// Avoid false-sharing by putting highly contended variables on their own cache lines |
|
||||
weak_atomic<size_t> front; // (Atomic) Elements are read from here |
|
||||
size_t localTail; // An uncontended shadow copy of tail, owned by the consumer |
|
||||
|
|
||||
char cachelineFiller0[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic<size_t>) - |
|
||||
sizeof(size_t)]; |
|
||||
weak_atomic<size_t> tail; // (Atomic) Elements are enqueued here |
|
||||
size_t localFront; |
|
||||
|
|
||||
char cachelineFiller1[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic<size_t>) - |
|
||||
sizeof(size_t)]; // next isn't very contended, but we don't want it on |
|
||||
// the same cache line as tail (which is) |
|
||||
weak_atomic<Block*> next; // (Atomic) |
|
||||
|
|
||||
char* data; // Contents (on heap) are aligned to T's alignment |
|
||||
|
|
||||
const size_t sizeMask; |
|
||||
|
|
||||
// size must be a power of two (and greater than 0) |
|
||||
AE_NO_TSAN Block(size_t const& _size, char* _rawThis, char* _data) |
|
||||
: front(0UL), localTail(0), tail(0UL), localFront(0), next(nullptr), data(_data), |
|
||||
sizeMask(_size - 1), rawThis(_rawThis) {} |
|
||||
|
|
||||
private: |
|
||||
// C4512 - Assignment operator could not be generated |
|
||||
Block& operator=(Block const&); |
|
||||
|
|
||||
public: |
|
||||
char* rawThis; |
|
||||
}; |
|
||||
|
|
||||
static Block* make_block(size_t capacity) AE_NO_TSAN { |
|
||||
// Allocate enough memory for the block itself, as well as all the elements it will contain |
|
||||
auto size = sizeof(Block) + std::alignment_of<Block>::value - 1; |
|
||||
size += sizeof(T) * capacity + std::alignment_of<T>::value - 1; |
|
||||
auto newBlockRaw = static_cast<char*>(std::malloc(size)); |
|
||||
if (newBlockRaw == nullptr) { |
|
||||
return nullptr; |
|
||||
} |
|
||||
|
|
||||
auto newBlockAligned = align_for<Block>(newBlockRaw); |
|
||||
auto newBlockData = align_for<T>(newBlockAligned + sizeof(Block)); |
|
||||
return new (newBlockAligned) Block(capacity, newBlockRaw, newBlockData); |
|
||||
} |
|
||||
|
|
||||
private: |
|
||||
weak_atomic<Block*> frontBlock; // (Atomic) Elements are dequeued from this block |
|
||||
|
|
||||
char cachelineFiller[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic<Block*>)]; |
|
||||
weak_atomic<Block*> tailBlock; // (Atomic) Elements are enqueued to this block |
|
||||
|
|
||||
size_t largestBlockSize; |
|
||||
|
|
||||
#ifndef NDEBUG |
|
||||
weak_atomic<bool> enqueuing; |
|
||||
mutable weak_atomic<bool> dequeuing; |
|
||||
#endif |
|
||||
}; |
|
||||
|
|
||||
// Like ReaderWriterQueue, but also providees blocking operations |
|
||||
template <typename T, size_t MAX_BLOCK_SIZE = 512> |
|
||||
class BlockingReaderWriterQueue { |
|
||||
private: |
|
||||
typedef ::Common::ReaderWriterQueue<T, MAX_BLOCK_SIZE> ReaderWriterQueue; |
|
||||
|
|
||||
public: |
|
||||
explicit BlockingReaderWriterQueue(size_t size = 15) AE_NO_TSAN |
|
||||
: inner(size), |
|
||||
sema(new spsc_sema::LightweightSemaphore()) {} |
|
||||
|
|
||||
BlockingReaderWriterQueue(BlockingReaderWriterQueue&& other) AE_NO_TSAN |
|
||||
: inner(std::move(other.inner)), |
|
||||
sema(std::move(other.sema)) {} |
|
||||
|
|
||||
BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue&& other) AE_NO_TSAN { |
|
||||
std::swap(sema, other.sema); |
|
||||
std::swap(inner, other.inner); |
|
||||
return *this; |
|
||||
} |
|
||||
|
|
||||
// Enqueues a copy of element if there is room in the queue. |
|
||||
// Returns true if the element was enqueued, false otherwise. |
|
||||
// Does not allocate memory. |
|
||||
AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN { |
|
||||
if (inner.try_enqueue(element)) { |
|
||||
sema->signal(); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// Enqueues a moved copy of element if there is room in the queue. |
|
||||
// Returns true if the element was enqueued, false otherwise. |
|
||||
// Does not allocate memory. |
|
||||
AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN { |
|
||||
if (inner.try_enqueue(std::forward<T>(element))) { |
|
||||
sema->signal(); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
// Like try_enqueue() but with emplace semantics (i.e. construct-in-place). |
|
||||
template <typename... Args> |
|
||||
AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN { |
|
||||
if (inner.try_emplace(std::forward<Args>(args)...)) { |
|
||||
sema->signal(); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
#endif |
|
||||
|
|
||||
// Enqueues a copy of element on the queue. |
|
||||
// Allocates an additional block of memory if needed. |
|
||||
// Only fails (returns false) if memory allocation fails. |
|
||||
AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN { |
|
||||
if (inner.enqueue(element)) { |
|
||||
sema->signal(); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// Enqueues a moved copy of element on the queue. |
|
||||
// Allocates an additional block of memory if needed. |
|
||||
// Only fails (returns false) if memory allocation fails. |
|
||||
AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN { |
|
||||
if (inner.enqueue(std::forward<T>(element))) { |
|
||||
sema->signal(); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
#if MOODYCAMEL_HAS_EMPLACE |
|
||||
// Like enqueue() but with emplace semantics (i.e. construct-in-place). |
|
||||
template <typename... Args> |
|
||||
AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN { |
|
||||
if (inner.emplace(std::forward<Args>(args)...)) { |
|
||||
sema->signal(); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
#endif |
|
||||
|
|
||||
// Attempts to dequeue an element; if the queue is empty, |
|
||||
// returns false instead. If the queue has at least one element, |
|
||||
// moves front to result using operator=, then returns true. |
|
||||
template <typename U> |
|
||||
bool try_dequeue(U& result) AE_NO_TSAN { |
|
||||
if (sema->tryWait()) { |
|
||||
bool success = inner.try_dequeue(result); |
|
||||
assert(success); |
|
||||
AE_UNUSED(success); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// Attempts to dequeue an element; if the queue is empty, |
|
||||
// waits until an element is available, then dequeues it. |
|
||||
template <typename U> |
|
||||
void wait_dequeue(U& result) AE_NO_TSAN { |
|
||||
while (!sema->wait()) |
|
||||
; |
|
||||
bool success = inner.try_dequeue(result); |
|
||||
AE_UNUSED(result); |
|
||||
assert(success); |
|
||||
AE_UNUSED(success); |
|
||||
} |
|
||||
|
|
||||
// Attempts to dequeue an element; if the queue is empty, |
|
||||
// waits until an element is available up to the specified timeout, |
|
||||
// then dequeues it and returns true, or returns false if the timeout |
|
||||
// expires before an element can be dequeued. |
|
||||
// Using a negative timeout indicates an indefinite timeout, |
|
||||
// and is thus functionally equivalent to calling wait_dequeue. |
|
||||
template <typename U> |
|
||||
bool wait_dequeue_timed(U& result, std::int64_t timeout_usecs) AE_NO_TSAN { |
|
||||
if (!sema->wait(timeout_usecs)) { |
|
||||
return false; |
|
||||
} |
|
||||
bool success = inner.try_dequeue(result); |
|
||||
AE_UNUSED(result); |
|
||||
assert(success); |
|
||||
AE_UNUSED(success); |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
#if __cplusplus > 199711L || _MSC_VER >= 1700 |
|
||||
// Attempts to dequeue an element; if the queue is empty, |
|
||||
// waits until an element is available up to the specified timeout, |
|
||||
// then dequeues it and returns true, or returns false if the timeout |
|
||||
// expires before an element can be dequeued. |
|
||||
// Using a negative timeout indicates an indefinite timeout, |
|
||||
// and is thus functionally equivalent to calling wait_dequeue. |
|
||||
template <typename U, typename Rep, typename Period> |
|
||||
inline bool wait_dequeue_timed(U& result, |
|
||||
std::chrono::duration<Rep, Period> const& timeout) AE_NO_TSAN { |
|
||||
return wait_dequeue_timed( |
|
||||
result, std::chrono::duration_cast<std::chrono::microseconds>(timeout).count()); |
|
||||
} |
|
||||
#endif |
|
||||
|
|
||||
// Returns a pointer to the front element in the queue (the one that |
|
||||
// would be removed next by a call to `try_dequeue` or `pop`). If the |
|
||||
// queue appears empty at the time the method is called, nullptr is |
|
||||
// returned instead. |
|
||||
// Must be called only from the consumer thread. |
|
||||
AE_FORCEINLINE T* peek() const AE_NO_TSAN { |
|
||||
return inner.peek(); |
|
||||
} |
|
||||
|
|
||||
// Removes the front element from the queue, if any, without returning it. |
|
||||
// Returns true on success, or false if the queue appeared empty at the time |
|
||||
// `pop` was called. |
|
||||
AE_FORCEINLINE bool pop() AE_NO_TSAN { |
|
||||
if (sema->tryWait()) { |
|
||||
bool result = inner.pop(); |
|
||||
assert(result); |
|
||||
AE_UNUSED(result); |
|
||||
return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// Returns the approximate number of items currently in the queue. |
|
||||
// Safe to call from both the producer and consumer threads. |
|
||||
AE_FORCEINLINE size_t size_approx() const AE_NO_TSAN { |
|
||||
return sema->availableApprox(); |
|
||||
} |
|
||||
|
|
||||
// Returns the total number of items that could be enqueued without incurring |
|
||||
// an allocation when this queue is empty. |
|
||||
// Safe to call from both the producer and consumer threads. |
|
||||
// |
|
||||
// NOTE: The actual capacity during usage may be different depending on the consumer. |
|
||||
// If the consumer is removing elements concurrently, the producer cannot add to |
|
||||
// the block the consumer is removing from until it's completely empty, except in |
|
||||
// the case where the producer was writing to the same block the consumer was |
|
||||
// reading from the whole time. |
|
||||
AE_FORCEINLINE size_t max_capacity() const { |
|
||||
return inner.max_capacity(); |
|
||||
} |
|
||||
|
|
||||
private: |
|
||||
// Disable copying & assignment |
|
||||
BlockingReaderWriterQueue(BlockingReaderWriterQueue const&) {} |
|
||||
BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue const&) {} |
|
||||
|
|
||||
private: |
|
||||
ReaderWriterQueue inner; |
|
||||
std::unique_ptr<spsc_sema::LightweightSemaphore> sema; |
|
||||
}; |
|
||||
|
|
||||
} // namespace Common |
|
||||
|
|
||||
#ifdef AE_VCPP |
|
||||
#pragma warning(pop) |
|
||||
#endif |
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue