Concurrent Collections
Vaibhav • September 11, 2025
In the previous article, we explored task parallelism-how to run multiple tasks concurrently and coordinate their results. But as soon as you start working with multiple threads or tasks, you run into a classic problem: shared data. If two tasks try to read and write to the same collection at the same time, you risk corrupting the data or crashing your application. That’s where concurrent collections come in.
In this article, we’ll explore the thread-safe collection types provided by .NET, including ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, and ConcurrentBag
. We’ll learn
how they work, when to use each one, and how they help you avoid race conditions and synchronization headaches
in multi-threaded environments.
Why Regular Collections Aren’t Safe
Most of the collections you’ve used so far-like List<T>
, Dictionary<K,V>
, and Queue<T>
-are
not thread-safe. If two threads try to modify a List<T>
at the same time,
you might get exceptions like InvalidOperationException
, or worse, corrupted
data.
You could use locks to protect access, but that adds complexity and can lead to deadlocks if not done carefully. Instead, .NET provides a set of collections designed specifically for concurrent access.
Concurrent collections use internal synchronization mechanisms like fine-grained locking or lock-free algorithms to ensure thread safety without requiring you to write your own locking code.
ConcurrentDictionary
ConcurrentDictionary<TKey, TValue>
is a thread-safe version of Dictionary<TKey, TValue>
. It allows multiple threads to read and write
without corrupting the data. It’s ideal for scenarios where you need fast lookups and updates from multiple
tasks.
ConcurrentDictionary scores = new ConcurrentDictionary();
scores["Alice"] = 10;
scores["Bob"] = 15;
// Safe update
scores.AddOrUpdate("Alice", 5, (key, oldValue) => oldValue + 5);
// Safe read
int bobScore = scores.GetOrAdd("Bob", 0);
Console.WriteLine($"Alice: {scores["Alice"]}, Bob: {bobScore}");
AddOrUpdate
ensures that the update is atomic-no other thread can interfere
during the operation. GetOrAdd
adds a value if the key doesn’t exist, or
returns the existing value.
ConcurrentQueue
ConcurrentQueue<T>
is a thread-safe FIFO (first-in, first-out) queue.
It’s perfect for producer-consumer scenarios where one task adds items and another removes them.
ConcurrentQueue messages = new ConcurrentQueue();
messages.Enqueue("Hello");
messages.Enqueue("World");
if (messages.TryDequeue(out string msg))
{
Console.WriteLine("Dequeued: " + msg);
}
TryDequeue
safely removes an item from the queue. If the queue is empty, it
returns false. You don’t need to worry about locking or synchronization.
ConcurrentStack
ConcurrentStack<T>
is a thread-safe LIFO (last-in, first-out) stack. It’s
useful when you need to reverse order or backtrack operations in a multi-threaded context.
ConcurrentStack history = new ConcurrentStack();
history.Push(1);
history.Push(2);
history.Push(3);
if (history.TryPop(out int last))
{
Console.WriteLine("Popped: " + last);
}
Push
adds an item to the top of the stack, and TryPop
removes it safely. You can also use TryPeek
to look at the top item without removing it.
ConcurrentBag
ConcurrentBag<T>
is a thread-safe unordered collection. It’s optimized
for scenarios where multiple threads add and remove items frequently, and order doesn’t matter.
ConcurrentBag tasks = new ConcurrentBag();
tasks.Add("Task1");
tasks.Add("Task2");
if (tasks.TryTake(out string task))
{
Console.WriteLine("Taken: " + task);
}
ConcurrentBag
is especially efficient when each thread works with its own local
copy. It uses thread-local storage internally to reduce contention.
Choosing the Right Concurrent Collection
Each concurrent collection is optimized for a specific access pattern. Choosing the right one depends on how you plan to use it:
Use ConcurrentDictionary
when you need key-based access and atomic updates. Use
ConcurrentQueue
for FIFO processing, ConcurrentStack
for LIFO, and ConcurrentBag
when
order doesn’t matter and performance is critical.
Avoid using regular collections like List<T>
or Dictionary<K,V>
in multi-threaded code unless you wrap them in locks.
Prefer concurrent collections for simplicity and safety.
Using Concurrent Collections with Parallel Tasks
Concurrent collections shine when used with parallel tasks. Here’s an example using ConcurrentBag
to collect results from multiple tasks:
ConcurrentBag results = new ConcurrentBag();
List tasks = new List();
for (int i = 0; i < 5; i++)
{
int value = i;
tasks.Add(Task.Run(() =>
{
int result = value * 10;
results.Add(result);
}));
}
await Task.WhenAll(tasks);
foreach (int r in results)
{
Console.WriteLine("Result: " + r);
}
Each task computes a value and adds it to the bag. Because ConcurrentBag
is
thread-safe, you don’t need to worry about synchronization.
Avoiding Common Pitfalls
While concurrent collections are safe, they’re not magic. You still need to think about your access patterns.
For example, don’t assume that TryDequeue
or TryPop
will always succeed-check the return value.
Also, avoid mixing concurrent and non-concurrent collections in the same workflow. If you copy data from a ConcurrentQueue
to a List<T>
, make sure no
other thread is modifying the queue during the copy.
You can use BlockingCollection<T>
to combine a
concurrent collection with blocking behavior. It’s useful for producer-consumer scenarios where you want to
wait until an item is available.
Summary
Concurrent collections are essential tools for writing safe and scalable multi-threaded applications. They
eliminate the need for manual locking and make it easy to share data across tasks. You’ve learned how to use
ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, and ConcurrentBag
, and how to
choose the right one for your scenario.
In the next article, we’ll explore Synchronization Primitives, which give you fine-grained
control over thread coordination using tools like Mutex
, Semaphore
, and Monitor
. These are useful when
concurrent collections aren’t enough and you need custom synchronization logic.