I implemented a queue capable of operating both in the FIFO mode and in the priority mode: when the priority mode is enabled, the elements are taken in order of decreasing priority; when the priority mode in disabled, the elements are taken in order of their arrival.
In order to manage the priority, I thought of using multiple queues (an array of Queue
objects, that is m_PriorityQueues
in the following code sample), one for each type of element; in this way, I can manage a priority based on the element type: just take first the elements from the queue at a higher priority and progressively from lower priority queues. In order to set the priority of different types of elements, I thought I'd pass an array of Type
objects in ascending order of priority, so that each Type
is associated with the index of a queue.
The user does not see multiple queues, but it uses the queue as if it were a single queue, so that the elements leave the queue in the order they arrive when the priority mode is disabled. In order to properly manage the switch from priority mode to FIFO mode, I have thought to use an additional queue (that is m_FifoOrder
field in the following code sample) to manage the order of arrival of the types of elements: essentially, when a new item is enqueued, it is added to the i-th queue, and the i value which indexes the array of queues is inserted to this additional queue of integers.
public class PriorityQueue
{
private Queue[] m_PriorityQueues;
private LinkedList<int> m_FifoOrder;
private Dictionary<Type, int> m_TypeMapping;
public PriorityQueue(Type[] prioritySet)
{
m_TypeMapping = new Dictionary<Type, int>();
for (int p = 0; p < prioritySet.Length; p++)
{
if (!m_TypeMapping.ContainsKey(prioritySet[p]))
m_TypeMapping.Add(prioritySet[p], p);
}
m_PriorityQueues = new Queue[m_TypeMapping.Count];
for (int i = 0; i < m_PriorityQueues.Length; i++)
m_PriorityQueues[i] = new Queue();
m_FifoOrder = new LinkedList<int>();
}
// Enable or disable the priority mode.
public bool IsPriorityEnabled { get; set; }
// Gets the priority count.
public int PriorityCount { get { return m_TypeMapping.Count; } }
// Gets the number of items actually enqueued in this queue.
public int Count { get { return m_FifoOrder.Count; } }
// Removes all objects from this queue.
public void Clear()
{
for (int i = 0; i < m_PriorityQueues.Length; i++)
m_PriorityQueues[i].Clear();
m_FifoOrder.Clear();
}
// Add an object to the end of this queue
public int Enqueue(object item)
{
int priority;
if (item == null)
{
priority = PriorityCount - 1; // higher priority
}
else if (!m_TypeMapping.TryGetValue(item.GetType(), out priority))
{
priority = 0; // lower priority for unknown types
}
m_PriorityQueues[priority].Enqueue(item);
m_FifoOrder.AddLast(priority);
return priority;
}
// Removes and returns the object at the beginning of this queue.
public bool TryDequeue(out object item)
{
if (IsPriorityEnabled)
{
for (int p = PriorityCount - 1; p >= 0; p--)
{
if (m_PriorityQueues[p].Count > 0)
{
item = m_PriorityQueues[p].Dequeue();
m_FifoOrder.Remove(p);
return true;
}
}
}
else
{
if (m_FifoOrder.Count > 0)
{
int index = m_FifoOrder.First.Value;
item = m_PriorityQueues[index].Dequeue();
m_FifoOrder.RemoveFirst();
return true;
}
}
item = null;
return false;
}
}
UPDATE: I performed some tests and I noticed that the TryDequeue
method of the version proposed above suffers from performance issues when the priority mode is enabled: the Remove
method, called on m_FifoOrder
linked list, performs a linear search, that is an O(n) operation. Obviously, the performance is reduced more so when n is very large.
In order to reduce the latency caused by this method, I created a new version of the priority queue: the FastPriorityQueue
class. The inner class ItemInfo
simply contains the object to be enqueued and the priority that is assigned during the queuing operation. An ItemInfo
object is always inserted at the end of the m_FifoOrder
linked list, so that the AddLast
method returns a reference to the last added LinkedListNode<ItemInfo>
: this reference is enqueued to one of the m_PriorityQueues
queues depending on the chosen priority.
public class FastPriorityQueue
{
private class ItemInfo
{
public object Data { get; set; }
public int Priority { get; set; }
}
private LinkedList<ItemInfo> m_FifoOrder;
private Queue<LinkedListNode<ItemInfo>>[] m_PriorityQueues;
private Dictionary<Type, int> m_TypeMapping;
public FastPriorityQueue(Type[] prioritySet)
{
m_TypeMapping = new Dictionary<Type, int>();
for (int p = 0; p < prioritySet.Length; p++)
{
if (!m_TypeMapping.ContainsKey(prioritySet[p]))
m_TypeMapping.Add(prioritySet[p], p);
}
m_PriorityQueues = new Queue<LinkedListNode<ItemInfo>>[m_TypeMapping.Count];
for (int i = 0; i < m_PriorityQueues.Length; i++)
m_PriorityQueues[i] = new Queue<LinkedListNode<ItemInfo>>();
m_FifoOrder = new LinkedList<ItemInfo>();
}
// Enable or disable the priority mode.
public bool IsPriorityEnabled { get; set; }
// Gets the priority count.
public int PriorityCount { get { return m_TypeMapping.Count; } }
// Gets the number of items actually enqueued in this queue.
public int Count { get { return m_FifoOrder.Count; } }
// Removes all objects from this queue.
public void Clear()
{
for (int i = 0; i < m_PriorityQueues.Length; i++)
m_PriorityQueues[i].Clear();
m_FifoOrder.Clear();
}
// Add an object to the end of this queue
public int Enqueue(object item)
{
int priority;
if (item == null)
{
priority = PriorityCount - 1; // higher priority
}
else if (!m_TypeMapping.TryGetValue(item.GetType(), out priority))
{
priority = 0; // lower priority for unknown types
}
LinkedListNode<ItemInfo> enqueued = m_FifoOrder.AddLast(
new ItemInfo
{
Data = item,
Priority = priority
});
m_PriorityQueues[priority].Enqueue(enqueued);
return priority;
}
// Removes and returns the object at the beginning of this queue.
public bool TryDequeue(out object item)
{
if (IsPriorityEnabled)
{
for (int p = PriorityCount - 1; p >= 0; p--)
{
if (m_PriorityQueues[p].Count > 0)
{
LinkedListNode<ItemInfo> dequeued = m_PriorityQueues[p].Dequeue();
item = dequeued.Value.Data;
m_FifoOrder.Remove(dequeued); // This method is an O(1) operation.
return true;
}
}
}
else
{
if (m_FifoOrder.Count > 0)
{
ItemInfo nodeItem = m_FifoOrder.First.Value;
item = nodeItem.Data;
m_PriorityQueues[nodeItem.Priority].Dequeue();
m_FifoOrder.RemoveFirst();
return true;
}
}
item = null;
return false;
}
}
Please review the above code samples and provide suggestions on how to improve them. Are there simpler solutions or more efficient than these? Also, if there are other solutions to switch between the two modes (FIFO mode or priority mode), please provide some details.
UPDATE 2: here is an example of initialization of a PriorityQueue
object. When the queue works in FIFO mode, therefore the priorities are ignored. Instead, when the priority mode is enabled, the next item removed from the queue depends on the priority of its type: items with the highest priority will be dequeued first.
// list of types in order of priority
Type[] priorities = new Type[] { typeof(ObjectWithLowerPriority), typeof(ObjectWithIntermediatePriority), typeof(ObjectWithHigherPriority) };
PriorityQueue queue = new PriorityQueue(priorities);
queue.Enqueue(...);
queue.Enqueue(...);
// ... other calls to Enqueue method
queue.IsPriorityEnabled = false;
queue.Dequeue();
// ...
queue.IsPriorityEnabled = true; // priority mode enabled
queue.Dequeue();
// ...
PriorityQueue
generic: see theEnqueue
method. If it's generic thenitem.GetType()
is alwaystypeof(T)
. \$\endgroup\$T
is an interface in my tests. Basically I could restore the previous version or constrainT
to be an interface or an abstract class. \$\endgroup\$T
is an interface or a non-sealed class, theitem.GetType()
can be different fromT
. \$\endgroup\$