Which members of .NET's ConcurrentDictionary are thread-safe?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
ConcurrentDictionary is built for concurrent access, but the phrase thread-safe is often misunderstood. In .NET, individual dictionary operations are synchronized, while multi-step workflows are still your responsibility. Knowing that boundary helps you avoid subtle race conditions even when using the right collection type.
What Is Thread-Safe in ConcurrentDictionary
The core methods on ConcurrentDictionary are designed for concurrent reads and writes. In practice, the following operations are safe to call from many threads at the same time:
TryAddTryRemoveTryGetValueTryUpdateGetOrAddAddOrUpdate- Indexer reads and writes, such as
dict[key]
The important point is that each of these operations is atomic at the dictionary level. If two threads call TryAdd with the same key, only one succeeds.
In this example, AddOrUpdate safely handles heavy parallel updates without external locks.
Delegates and Atomicity Boundaries
GetOrAdd and AddOrUpdate accept delegates. The dictionary guarantees the final mutation is thread-safe, but it does not guarantee your delegate runs only once globally. Under contention, a value factory can be executed more than once, and only one result may win.
That means delegates should be side-effect free whenever possible.
You may see factoryCalls greater than 1. This is expected behavior and not a bug in ConcurrentDictionary.
Enumeration and Snapshot Semantics
Enumeration is safe in the sense that it will not corrupt internal state or throw because of concurrent writes. However, it is not a transactionally consistent snapshot of all writes that happen during the loop.
If you need a stable view for reporting or serialization, materialize a copy first with ToArray() and process that copy.
Extension Methods and Interface-Based Access
The thread-safety guarantees apply to the concrete ConcurrentDictionary members. If you cast to interfaces like IDictionary<TKey, TValue> and call members not designed for concurrent mutation, behavior can be less obvious and may involve external assumptions.
Similarly, LINQ over a live dictionary is safe for simple projection but still subject to changing data during query execution. For deterministic results, snapshot first.
Common Pitfalls
- Assuming thread-safe means multi-step logic is automatically safe. Fix: Use one atomic method when possible, or add your own lock around the full sequence.
- Putting side effects inside
GetOrAddorAddOrUpdatedelegates. Fix: Keep delegates pure, or make side effects idempotent. - Treating enumeration as a transactionally complete snapshot. Fix: Use
ToArray()before analysis or export. - Mixing
ContainsKeyfollowed by indexer assignment in separate steps. Fix: PreferTryAdd,TryUpdate, orAddOrUpdateto avoid races. - Using external mutable objects as dictionary values without their own synchronization. Fix: Store immutable values, or protect mutable internals separately.
Summary
ConcurrentDictionaryprovides thread-safe atomic operations for its key methods.TryAdd,TryRemove,TryGetValue,TryUpdate,GetOrAdd, andAddOrUpdateare safe under concurrency.- Delegate factories may run multiple times, so avoid side effects in those delegates.
- Enumeration is safe but not a strict point-in-time transaction snapshot.
- Multi-step workflows still need explicit synchronization or redesign around single atomic calls.

