Thread-Safety
Dictionary
Concurrency
Data Structures
Multi-threading

What's the best way of implementing a thread-safe Dictionary?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

A normal dictionary is not safe for concurrent reads and writes unless the language runtime explicitly says otherwise. In .NET, the best implementation depends on the access pattern, but for most multi-threaded application code the right default is ConcurrentDictionary<TKey, TValue> rather than a manually locked Dictionary<TKey, TValue>.

Why a Plain Dictionary Is Not Enough

A standard dictionary assumes that one thread controls mutation at a time. If one thread is adding or removing entries while another is reading or enumerating, you can get race conditions, exceptions, or corrupted logical behavior.

Wrapping every operation in a lock can make the structure safe, but it serializes access and pushes concurrency correctness onto your own code. That is fine for very small or simple cases, but it becomes fragile when compound operations appear.

The Best General-Purpose Choice: ConcurrentDictionary

ConcurrentDictionary<TKey, TValue> is designed for concurrent access. It gives you thread-safe reads and writes plus atomic helpers for common patterns such as add-if-missing and update-if-present.

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Threading.Tasks;
4
5class Program
6{
7    static async Task Main()
8    {
9        var counts = new ConcurrentDictionary<string, int>();
10
11        Task[] tasks = new Task[3];
12        for (int i = 0; i < tasks.Length; i++)
13        {
14            tasks[i] = Task.Run(() =>
15            {
16                for (int j = 0; j < 1000; j++)
17                {
18                    counts.AddOrUpdate("apple", 1, (_, oldValue) => oldValue + 1);
19                }
20            });
21        }
22
23        await Task.WhenAll(tasks);
24        Console.WriteLine(counts["apple"]);
25    }
26}

The important part is AddOrUpdate. It performs the read-modify-write cycle atomically, which is hard to reproduce safely with ad hoc locking if the codebase grows.

When a Simple lock Is Still Reasonable

If the dictionary is small, contention is low, and the operations are simple, a plain Dictionary<TKey, TValue> protected by one lock can be perfectly acceptable. The code is easy to understand and can be faster than a more sophisticated structure when concurrency is minimal.

csharp
1using System;
2using System.Collections.Generic;
3
4class SafeCache
5{
6    private readonly Dictionary<string, string> _data = new Dictionary<string, string>();
7    private readonly object _gate = new object();
8
9    public void Set(string key, string value)
10    {
11        lock (_gate)
12        {
13            _data[key] = value;
14        }
15    }
16
17    public bool TryGet(string key, out string value)
18    {
19        lock (_gate)
20        {
21            return _data.TryGetValue(key, out value);
22        }
23    }
24}

This approach is often good enough for background tools or small desktop apps. It becomes less attractive when you need high write throughput or many independent readers.

Read-Mostly Data: Immutable Collections

A third option is immutability. If updates are rare and reads dominate, an immutable dictionary can remove most locking concerns. Instead of changing the existing collection, you replace the entire reference with a new version.

That pattern is common for configuration snapshots, routing tables, and application metadata that changes occasionally but is read constantly.

Choosing the Right Tool

A useful rule of thumb is:

  • use ConcurrentDictionary<TKey, TValue> for general shared mutable state
  • use lock with Dictionary<TKey, TValue> when the concurrency requirements are small and simple
  • use immutable collections for read-heavy workloads with infrequent updates

The best solution is about behavior, not fashion. Picking a “thread-safe dictionary” without understanding the access pattern is how subtle bugs survive review.

Common Pitfalls

The most common mistake is using a thread-safe container but performing non-atomic logic around it. For example, code that checks ContainsKey, then computes a value, then assigns the value is still racy unless it uses an atomic helper such as GetOrAdd.

Another pitfall is assuming enumeration gives a perfectly frozen snapshot. Concurrent collections support safe enumeration, but the observed contents may reflect some updates and not others depending on timing. That is usually fine, but it matters if you expected a transactional view.

A manually locked dictionary also fails when not every access follows the same lock discipline. One missing lock in a helper method is enough to reintroduce the race.

Finally, do not optimize too early. Replacing a simple lock with a more complex structure only makes sense if you actually have contention or scalability requirements. The goal is correctness first, then throughput.

Summary

  • 'ConcurrentDictionary<TKey, TValue> is the best default for a shared mutable dictionary in .NET.'
  • A plain dictionary plus one lock is still valid for small, low-contention code paths.
  • Immutable dictionaries are strong options when reads dominate and updates are rare.
  • Thread-safe containers do not make multi-step logic atomic unless you use the right API.
  • Choose the structure based on read and write patterns, not on general preference alone.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

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

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.