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.
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.
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.
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
lockwithDictionary<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
lockis 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
- What's the cleanest way of applying map to a dictionary in Swift?
- What's the diff between tf.import_graph_def and tf.train.import_meta_graph
- What's the difference between a ReadOnlyDictionary and an ImmutableDictionary?
- What's the difference between and vs list and dict?
- What's the best way to asynchronously handle low-speed consumer database in high performance Java application
- What's the best way to update an ObservableCollection from another thread?
- What’s the difference between Array() and [] while declaring a JavaScript array?
- What's the difference between ConcurrentHashMap and Collections.synchronizedMap(Map)?

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 courseTrack 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.