concurrent programming
.NET
thread safety
collection classes
multithreading

Thread safe collections in .NET

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Standard .NET collections such as List<T> and Dictionary<TKey, TValue> are not safe for concurrent mutation. If multiple threads add, remove, or update items at the same time, you need either explicit synchronization or collection types designed for concurrent access.

The best choice depends on the access pattern. A queue, a dictionary, an unordered bag, and an immutable snapshot all solve different concurrency problems.

Why Regular Collections Are Not Enough

A simple lock can make ordinary collections safe:

csharp
1using System.Collections.Generic;
2using System.Threading.Tasks;
3
4var list = new List<int>();
5
6Parallel.For(0, 1000, i =>
7{
8    lock (list)
9    {
10        list.Add(i);
11    }
12});

This works, but a single coarse lock can become a contention bottleneck. It also puts the burden of correctness on every caller. That is why .NET provides concurrent collection types in System.Collections.Concurrent.

Use ConcurrentDictionary for Shared Keyed State

ConcurrentDictionary<TKey, TValue> is the usual choice for shared maps:

csharp
1using System.Collections.Concurrent;
2using System.Threading.Tasks;
3
4var counts = new ConcurrentDictionary<string, int>();
5
6Parallel.For(0, 10000, _ =>
7{
8    counts.AddOrUpdate("jobs", 1, (_, oldValue) => oldValue + 1);
9});
10
11Console.WriteLine(counts["jobs"]);

The key advantage is not just thread safety. It is the availability of atomic methods such as:

  • 'GetOrAdd'
  • 'AddOrUpdate'
  • 'TryUpdate'

Those avoid fragile multi-step read-modify-write sequences.

Use Queue, Stack, and Bag Types for Workflows

The concurrent namespace also includes types optimized for specific semantics:

csharp
1using System.Collections.Concurrent;
2
3var queue = new ConcurrentQueue<int>();
4queue.Enqueue(1);
5queue.Enqueue(2);
6
7if (queue.TryDequeue(out var item))
8{
9    Console.WriteLine(item);
10}

Common choices are:

  • 'ConcurrentQueue<T> for FIFO work pipelines'
  • 'ConcurrentStack<T> for LIFO behavior'
  • 'ConcurrentBag<T> for unordered accumulation'

Choose by semantics first, not just by habit.

Use BlockingCollection for Producer-Consumer Pipelines

When you need coordination and backpressure, BlockingCollection<T> adds useful behavior on top of a concurrent collection:

csharp
1using System.Collections.Concurrent;
2using System.Threading.Tasks;
3
4var buffer = new BlockingCollection<int>(boundedCapacity: 100);
5
6var producer = Task.Run(() =>
7{
8    for (int i = 0; i < 1000; i++) buffer.Add(i);
9    buffer.CompleteAdding();
10});
11
12var consumer = Task.Run(() =>
13{
14    foreach (var n in buffer.GetConsumingEnumerable())
15    {
16        // process n
17    }
18});
19
20Task.WaitAll(producer, consumer);

This is useful when producers should slow down once a bounded queue is full or when consumers need a clear completion signal.

Immutable Collections Solve a Different Problem

Sometimes the real need is not concurrent mutation but safe sharing of read-only snapshots. In those cases, immutable collections can be a better fit than concurrent mutable ones.

An immutable collection is useful when:

  • many threads mostly read
  • updates are infrequent
  • replacing the whole snapshot is acceptable

That is a different design from a hot concurrent queue or dictionary, and it is worth recognizing the difference early.

Enumeration Is Safe, Not Transactional

Concurrent collection enumeration is usually thread-safe, but that does not mean it represents one perfectly frozen transaction view. If you need a stable snapshot, create one explicitly:

csharp
var snapshot = counts.ToArray();

That avoids subtle bugs where code assumes enumeration sees an exact final state while other threads are still updating the collection.

Common Pitfalls

The biggest mistake is wrapping a concurrent collection in another coarse lock and accidentally giving up most of its scaling benefit.

Another common issue is assuming that using a concurrent collection makes every multi-step workflow atomic. It does not. One atomic method is safe; a sequence of separate operations may still need additional coordination.

Developers also choose ConcurrentBag<T> when they really need deterministic order. That collection is for unordered scenarios, not queue semantics.

Finally, do not assume thread-safe enumeration means transactionally consistent enumeration. If you need a fixed view, take a snapshot explicitly.

Summary

  • Use concurrent collections in .NET when multiple threads mutate shared state.
  • 'ConcurrentDictionary is the standard choice for shared keyed data with atomic update methods.'
  • 'ConcurrentQueue, ConcurrentStack, and ConcurrentBag solve different workflow semantics.'
  • 'BlockingCollection adds coordination and bounded producer-consumer behavior.'
  • Pick the collection type based on semantics and concurrency pattern, not just on familiarity.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.