C# Queue Enqueue and Dequeue: Usage and Performance
c# queue enqueue dequeue: Learn how to use Queue<T> Enqueue and Dequeue in C# with practical examples, thread-safety options, and performance considerations for FIFO p...
The Queue<T> class in C# implements a first-in, first-out (FIFO) collection, and its Enqueue and Dequeue methods are the primary way to add and remove items. Understanding how these methods behave, including their edge cases and performance characteristics, is essential for building correct and efficient code. This article covers the core operations, safe alternatives, thread-safety, and practical usage patterns for c# queue enqueue dequeue scenarios.
When to Use a Queue<T> in C#
A Queue<T> is the right choice when you need to process items in the exact order they arrive. Common examples include job scheduling, request buffering, breadth-first search, and message processing. The FIFO guarantee is the defining characteristic: the first item added is the first item removed. If your data access pattern is last-in, first-out (LIFO), you should use a Stack<T> instead. If you need random access by index, a List<T> or array is more appropriate.
The type Queue<T> is part of System.Collections.Generic and is available in all modern .NET versions. It stores elements of a single type T, which gives you compile-time type safety without casting.
Enqueue and Dequeue: Core Operations
The Enqueue method adds an item to the end of the queue. The Dequeue method removes and returns the item at the beginning of the queue. Here is a minimal example:
Queue<string> tasks = new Queue<string>(); tasks.Enqueue("first"); tasks.Enqueue("second"); tasks.Enqueue("third"); string next = tasks.Dequeue(); // returns "first" Console.WriteLine(next); // first Console.WriteLine(tasks.Count); // 2
After the first Dequeue, the queue contains "second" and "third". Each subsequent call removes the next item in insertion order. If you call Dequeue when the queue is empty, it throws an InvalidOperationException. This is a common source of bugs, especially in multithreaded or event-driven code where the queue may be drained by multiple consumers.
The Enqueue method never throws an exception for a null reference if T is a reference type; it simply stores null. If your logic assumes non-null items, you need to check before enqueuing or handle null when dequeuing.
Peeking Without Removing Items
Sometimes you need to inspect the next item without removing it. The Peek method returns the item at the front of the queue without altering the queue. It also throws InvalidOperationException if the queue is empty.
Queue<int> numbers = new Queue<int>(); numbers.Enqueue(10); numbers.Enqueue(20); int front = numbers.Peek(); // 10 Console.WriteLine(numbers.Count); // still 2
Peek is useful when you want to conditionally process the next item based on its value, but you don't want to remove it until you are ready. However, in a concurrent scenario, Peek is not atomic with Dequeue; another thread could remove the item between your Peek and Dequeue calls.
Safe Access with TryDequeue and TryPeek
To avoid exceptions when the queue is empty, use TryDequeue and TryPeek. These methods return a bool indicating success and provide the item via an out parameter. They are the recommended way to drain a queue in a loop, especially when multiple consumers are involved.
Queue<int> queue = new Queue<int>(); queue.Enqueue(1); queue.Enqueue(2); while (queue.TryDequeue(out int item)) { Console.WriteLine(item); } // TryPeek example if (queue.TryPeek(out int first)) { Console.WriteLine($"Next: {first}"); }
TryDequeue and TryPeek do not throw on an empty queue; they simply return false and set the out parameter to the default value of T. This makes them ideal for consumer loops where you cannot guarantee that items are always available.
Thread Safety with ConcurrentQueue<T>
The standard Queue<T> is not thread-safe. If multiple threads call Enqueue or Dequeue concurrently without external synchronization, the internal state can become corrupted, leading to lost items, exceptions, or infinite loops. For multithreaded producer-consumer scenarios, use ConcurrentQueue<T> from System.Collections.Concurrent. It is designed for safe concurrent access and uses lock-free techniques internally for high-throughput scenarios.
ConcurrentQueue<string> messages = new ConcurrentQueue<string>(); // Producer messages.Enqueue("message 1"); messages.Enqueue("message 2"); // Consumer while (messages.TryDequeue(out string msg)) { Console.WriteLine(msg); }
ConcurrentQueue<T> provides Enqueue, TryDequeue, TryPeek, and Count (which is approximate). It does not have a blocking Dequeue; you need to implement waiting yourself if you want consumers to block until an item arrives. For a blocking queue, consider using System.Threading.Channels or BlockingCollection<T>.
Performance and Memory Behavior
Queue<T> is backed by an internal array that grows as needed. Enqueue is an O(1) operation on average, but when the internal array is full, it resizes to a larger capacity, which is O(n) because all existing elements are copied. Dequeue is O(1) because it simply moves the head index and clears the old slot. However, the internal array is not compacted when items are dequeued; the head index advances, and the unused space at the front is reclaimed only when the queue is resized or when the head reaches the end and the array is reused.
This means a Queue<T> that is repeatedly filled and emptied can hold memory for the larger capacity even if the count is small. If you have a queue that grows to a large size and then shrinks, the memory may not be released until the queue is discarded. For long-lived queues with variable load, consider calling TrimExcess() to reduce capacity to match the current count, but be aware that this forces a reallocation.
The following table summarizes the time complexity of common operations on Queue<T> compared to List<T>:
| Operation | Queue<T> | List<T> |
|---|---|---|
| Add to end | O(1) average | O(1) average |
| Remove from front | O(1) | O(n) (shift) |
| Access by index | O(n) (no index) | O(1) |
| Search | O(n) | O(n) |
For FIFO processing, Queue<T> is clearly more efficient than List<T> because removing the first element from a List<T> requires shifting all subsequent elements. If you need both FIFO and random access, consider a LinkedList<T> or a custom ring buffer.
Queue Capacity and Growth
Queue<T> starts with a default capacity of 0 and grows to 4, then 8, 16, and so on, doubling each time it needs more space. You can specify an initial capacity in the constructor if you know the approximate number of elements, which reduces the number of resizes and improves performance.
Queue<int> queue = new Queue<int>(1000);
When the internal array is full, Enqueue allocates a new array with twice the current capacity and copies all elements. This is an expensive operation, so setting a reasonable initial capacity for large queues can avoid multiple resizes. The Count property returns the number of elements currently in the queue, while the internal capacity is not directly exposed.
Practical Example: Processing Jobs in FIFO Order
A common use case is a job processor that reads from a queue and processes each item. The following example shows a simple single-threaded worker that uses TryDequeue to process all pending jobs:
class JobProcessor { private readonly Queue<Action> _jobs = new Queue<Action>(); public void AddJob(Action job) { _jobs.Enqueue(job); } public void ProcessAll() { while (_jobs.TryDequeue(out Action job)) { job(); } } }
In a multithreaded environment, you would replace the Queue<Action> with a ConcurrentQueue<Action> and ensure that AddJob and ProcessAll can be called from different threads. The TryDequeue loop is safe because it checks for an empty queue without throwing, and it will terminate when the queue is empty.
One subtlety is that TryDequeue returns false immediately if the queue is empty. In a producer-consumer scenario, you might want the consumer to wait for new items instead of busy-looping. You can use a SemaphoreSlim or a Channel<T> to implement blocking behavior, but that goes beyond the basic Queue<T> API.
Another edge case is when you need to dequeue all items but also preserve the order for later processing. You can simply iterate with TryDequeue and store the items in a list. The FIFO order is guaranteed, so the resulting list will be in the same order as the original queue.
Understanding the behavior of Enqueue and Dequeue is not just about syntax; it is about knowing when to use the right collection and how to avoid common pitfalls like empty-queue exceptions and thread-safety issues. The Queue<T> class is a small but important tool in the .NET collection library, and using it correctly makes your code more predictable and maintainable.