//****************************************************************************************************** // ConcurrentIsolatedQueue.cs - Gbtc // // Copyright © 2016, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 11/28/2016 - Steven E. Chisholm // Generated original version of source code based on IsolatedQueue // //****************************************************************************************************** using System; using System.Threading; namespace GSF.Collections { /// /// Provides a buffer of point data where reads are isolated from writes. /// This class differs from because writes can be concurrent. /// Reads on the other hand have to be externally synchronized. /// public class ConcurrentIsolatedQueue where T : class { private static readonly DynamicObjectPool Pool = new DynamicObjectPool(() => new IsolatedNode(), 10); class IsolatedNode { /// /// The slots to put stuff. /// public readonly T[] Slots; /// /// The next node of the linked list. /// public IsolatedNode NextNode; /// /// The base index of this node /// public long PositionIndex; /// /// Creates a /// public IsolatedNode() { Slots = new T[NodeSize]; } } private const int NodeSize = 128; private const int ShiftBits = 7; private const int BitMask = 127; private IsolatedNode m_currentHead; private IsolatedNode m_currentTail; private long m_enqueueCount; private long m_dequeueCount; /// /// Creates an /// public ConcurrentIsolatedQueue() { m_currentHead = m_currentTail = new IsolatedNode(); m_currentHead.PositionIndex = 0; } /// /// The number of elements in the queue. /// /// /// Note: Due to the nature of concurrency. This is a representative number. /// and does not mean the exact number of items in the queue unless both Enqueue and Dequeue /// are not currently processing. /// public int Count { get { if (Environment.Is64BitProcess) { return Math.Max(0, (int)(m_enqueueCount - m_dequeueCount)); } return Math.Max(0, (int)(Interlocked.Read(ref m_enqueueCount) - Interlocked.Read(ref m_dequeueCount))); } } /// /// Adds the provided item to the . This method is thread safe. /// /// cannot be null public void Enqueue(T item) { if (item == null) throw new ArgumentNullException(nameof(item)); //I must acquire the current head before I increment m_enqueueCount //This effectively takes a snapshot before the increment operation occurs //If I waited until after the increment, then it's possible that multiple //nodes have passed and I'd never be able to find the one needed to //assign this item. IsolatedNode currentNode = m_currentHead; Thread.MemoryBarrier(); long index = Interlocked.Increment(ref m_enqueueCount) - 1; long positionIndex = index >> ShiftBits; int arrayIndex = (int)(index & BitMask); while (true) { // ReSharper disable once PossibleNullReferenceException if (currentNode.PositionIndex == positionIndex) { currentNode.Slots[arrayIndex] = item; return; } if (currentNode.NextNode == null) { Grow(currentNode); } currentNode = currentNode.NextNode; } } private void Grow(IsolatedNode currentNode) { //Each thread is in a race to grow the current node. This is to ensure that //Enqueue will never block. //The person who wins the race will assign m_current with the node they placed. //while it's possible that a race condition could assign m_currentHead out of sequence //eventually it will catch back up and there will be little performance penalty in the mean time. //Note: There is a very small chance of ConcurrentQueue blocking here. But that's ok. IsolatedNode nextNode = Pool.Dequeue(); nextNode.PositionIndex = currentNode.PositionIndex + 1; if (Interlocked.CompareExchange(ref currentNode.NextNode, nextNode, null) == null) { m_currentHead = nextNode; } else { Pool.Enqueue(nextNode); } } /// /// Attempts to dequeue the specified item from the . This method is NOT thread safe. /// /// an output for the item /// /// /// During a race condition, the queue might not be completely empty when TryDequeue returns false. Instead this method returns false /// rather than blocking and waiting on the race condition to satisfy. /// public bool TryDequeue(out T item) { long positionIndex = m_dequeueCount >> ShiftBits; int arrayIndex = (int)(m_dequeueCount & BitMask); TryAgain: if (m_currentTail.PositionIndex == positionIndex) { item = m_currentTail.Slots[arrayIndex]; if (item == null) return false; m_currentTail.Slots[arrayIndex] = null; if (Environment.Is64BitProcess) m_dequeueCount++; else Interlocked.Increment(ref m_dequeueCount); return true; } if (m_currentTail.NextNode == null) { item = null; return false; } IsolatedNode node = m_currentTail; m_currentTail = m_currentTail.NextNode; node.NextNode = null; Pool.Enqueue(node); goto TryAgain; } /// /// Adds the provided items to the . /// /// the items to add /// the offset position /// the length public void Enqueue(T[] items, int offset, int length) { items.ValidateParameters(offset, length); for (int x = 0; x < length; x++) { Enqueue(items[offset + x]); } } /// /// Dequeues all of the items into the provided array /// /// where to put the items /// the starting index /// the maximum number of times to store /// the number of items dequeued public int Dequeue(T[] items, int startingIndex, int length) { items.ValidateParameters(startingIndex, length); for (int x = 0; x < length; x++) { T item; if (TryDequeue(out item)) { items[x] = item; } else { return x; } } return length; } } }