| // | |
| // Copyright 2023 The ANGLE Project Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| // | |
| // FixedQueue.h: | |
| // An array based fifo queue class that supports concurrent push and pop. | |
| // | |
| #ifndef COMMON_FIXEDQUEUE_H_ | |
| #define COMMON_FIXEDQUEUE_H_ | |
| #include "common/debug.h" | |
| #include <algorithm> | |
| #include <array> | |
| #include <atomic> | |
| namespace angle | |
| { | |
| // class FixedQueue: An vector based fifo queue class that supports concurrent push and | |
| // pop. Caller must ensure queue is not empty before pop and not full before push. This class | |
| // supports concurrent push and pop from different threads, but only with single producer single | |
| // consumer usage. If caller want to push from two different threads, proper mutex must be used to | |
| // ensure the access is serialized. You can also call updateCapacity to adjust the storage size, but | |
| // caller must take proper mutex lock to ensure no one is accessing the storage. In a typical usage | |
| // case is that you have two mutex lock, enqueueLock and dequeueLock. You use enqueueLock to push | |
| // and use dequeueLock to pop. You dont need the lock for checking size (i.e, call empty/full). You | |
| // take both lock in a given order to call updateCapacity. See unit test | |
| // FixedQueue.ConcurrentPushPopWithResize for example. | |
| template <class T> | |
| class FixedQueue final : angle::NonCopyable | |
| { | |
| public: | |
| using Storage = std::vector<T>; | |
| using value_type = typename Storage::value_type; | |
| using size_type = typename Storage::size_type; | |
| using reference = typename Storage::reference; | |
| using const_reference = typename Storage::const_reference; | |
| FixedQueue(size_t capacity); | |
| ~FixedQueue(); | |
| size_type size() const; | |
| bool empty() const; | |
| bool full() const; | |
| size_type capacity() const; | |
| // Caller must ensure no one is accessing the data while update storage. This should happen | |
| // infrequently since all data will be copied between old storage and new storage. | |
| void updateCapacity(size_t newCapacity); | |
| reference front(); | |
| const_reference front() const; | |
| void push(const value_type &value); | |
| void push(value_type &&value); | |
| reference back(); | |
| const_reference back() const; | |
| void pop(); | |
| void clear(); | |
| private: | |
| Storage mData; | |
| // The front and back indices are virtual indices (think about queue sizd is infinite). They | |
| // will never wrap around when hit N. The wrap around occur when element is referenced. Virtual | |
| // index for current head | |
| size_type mFrontIndex; | |
| // Virtual index for next write. | |
| size_type mEndIndex; | |
| // Atomic so that we can support concurrent push and pop. | |
| std::atomic<size_type> mSize; | |
| size_type mMaxSize; | |
| }; | |
| template <class T> | |
| FixedQueue<T>::FixedQueue(size_t capacity) | |
| : mFrontIndex(0), mEndIndex(0), mSize(0), mMaxSize(capacity) | |
| { | |
| mData.resize(mMaxSize); | |
| } | |
| template <class T> | |
| FixedQueue<T>::~FixedQueue() | |
| { | |
| mData.clear(); | |
| } | |
| template <class T> | |
| ANGLE_INLINE typename FixedQueue<T>::size_type FixedQueue<T>::size() const | |
| { | |
| return mSize; | |
| } | |
| template <class T> | |
| ANGLE_INLINE bool FixedQueue<T>::empty() const | |
| { | |
| return mSize == 0; | |
| } | |
| template <class T> | |
| ANGLE_INLINE bool FixedQueue<T>::full() const | |
| { | |
| return mSize >= mMaxSize; | |
| } | |
| template <class T> | |
| ANGLE_INLINE typename FixedQueue<T>::size_type FixedQueue<T>::capacity() const | |
| { | |
| return mMaxSize; | |
| } | |
| template <class T> | |
| ANGLE_INLINE void FixedQueue<T>::updateCapacity(size_t newCapacity) | |
| { | |
| ASSERT(newCapacity >= mSize); | |
| Storage newData(newCapacity); | |
| for (size_type i = mFrontIndex; i < mEndIndex; i++) | |
| { | |
| newData[i % newCapacity] = std::move(mData[i % mMaxSize]); | |
| } | |
| mData.clear(); | |
| std::swap(newData, mData); | |
| mMaxSize = newCapacity; | |
| ASSERT(mData.size() == mMaxSize); | |
| } | |
| template <class T> | |
| ANGLE_INLINE typename FixedQueue<T>::reference FixedQueue<T>::front() | |
| { | |
| ASSERT(mSize > 0); | |
| return mData[mFrontIndex % mMaxSize]; | |
| } | |
| template <class T> | |
| ANGLE_INLINE typename FixedQueue<T>::const_reference FixedQueue<T>::front() const | |
| { | |
| ASSERT(mSize > 0); | |
| return mData[mFrontIndex % mMaxSize]; | |
| } | |
| template <class T> | |
| void FixedQueue<T>::push(const value_type &value) | |
| { | |
| ASSERT(mSize < mMaxSize); | |
| mData[mEndIndex % mMaxSize] = value; | |
| mEndIndex++; | |
| // We must increment size last, after we wrote data. That way if another thread is doing | |
| // `if(!dq.empty()){ s = dq.front(); }`, it will only see not empty until element is fully | |
| // pushed. | |
| mSize++; | |
| } | |
| template <class T> | |
| void FixedQueue<T>::push(value_type &&value) | |
| { | |
| ASSERT(mSize < mMaxSize); | |
| mData[mEndIndex % mMaxSize] = std::move(value); | |
| mEndIndex++; | |
| // We must increment size last, after we wrote data. That way if another thread is doing | |
| // `if(!dq.empty()){ s = dq.front(); }`, it will only see not empty until element is fully | |
| // pushed. | |
| mSize++; | |
| } | |
| template <class T> | |
| ANGLE_INLINE typename FixedQueue<T>::reference FixedQueue<T>::back() | |
| { | |
| ASSERT(mSize > 0); | |
| return mData[(mEndIndex + (mMaxSize - 1)) % mMaxSize]; | |
| } | |
| template <class T> | |
| ANGLE_INLINE typename FixedQueue<T>::const_reference FixedQueue<T>::back() const | |
| { | |
| ASSERT(mSize > 0); | |
| return mData[(mEndIndex + (mMaxSize - 1)) % mMaxSize]; | |
| } | |
| template <class T> | |
| void FixedQueue<T>::pop() | |
| { | |
| ASSERT(mSize > 0); | |
| mData[mFrontIndex % mMaxSize] = value_type(); | |
| mFrontIndex++; | |
| // We must decrement size last, after we wrote data. That way if another thread is doing | |
| // `if(!dq.full()){ dq.push; }`, it will only see not full until element is fully popped. | |
| mSize--; | |
| } | |
| template <class T> | |
| void FixedQueue<T>::clear() | |
| { | |
| // Size will change in the "pop()" and also by "push()" calls from other thread. | |
| const size_type localSize = mSize; | |
| for (size_type i = 0; i < localSize; i++) | |
| { | |
| pop(); | |
| } | |
| } | |
| } // namespace angle | |
| #endif // COMMON_FIXEDQUEUE_H_ |