concurrent dictionary
dictionary usage
multithreading
C# programming
thread safety

Concurrent Dictionary Correct Usage

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

ConcurrentDictionary is the right choice when many threads need shared key-value access without a single coarse lock around a normal Dictionary. The most important rule is to use its atomic APIs such as GetOrAdd, TryUpdate, and AddOrUpdate instead of building your own check-then-act sequences around it.

Use Atomic Operations, Not Split Logic

This is the pattern you should avoid:

csharp
1if (!dict.ContainsKey(key))
2{
3    dict[key] = value;
4}

Even with ConcurrentDictionary, that sequence is not one atomic operation. Another thread can modify the dictionary between the check and the assignment.

Prefer the built-in atomic method:

csharp
1using System.Collections.Concurrent;
2
3var dict = new ConcurrentDictionary<string, int>();
4int value = dict.GetOrAdd("retries", 0);

That guarantees the add-or-read behavior happens safely under concurrency.

AddOrUpdate and TryUpdate

When a value must be updated based on the existing value, use the APIs designed for that:

csharp
1using System.Collections.Concurrent;
2
3var counts = new ConcurrentDictionary<string, int>();
4
5counts.AddOrUpdate(
6    "jobs",
7    addValueFactory: _ => 1,
8    updateValueFactory: (_, current) => current + 1
9);

For compare-and-swap style logic, TryUpdate is useful:

csharp
var ok = counts.TryUpdate("jobs", 10, comparisonValue: 9);

This updates the value only if the current value is still 9.

Value Factories May Run More Than Once

One subtle point surprises many developers: the delegate you pass to GetOrAdd or AddOrUpdate can be invoked more than once under contention, even though only one resulting value wins.

That means the factory should be:

  • side-effect free
  • cheap, if possible
  • safe to run more than once

This is correct:

csharp
var cache = new ConcurrentDictionary<string, string>();

string value = cache.GetOrAdd("language", _ => "C#");

This is risky if LoadFromDatabase() has side effects or is very expensive:

csharp
string value = cache.GetOrAdd("user:42", _ => LoadFromDatabase());

You may still need an outer lazy-initialization pattern if the creation cost is significant.

One common pattern is storing Lazy<T> inside the dictionary so the dictionary coordinates key access while Lazy<T> coordinates one-time initialization of the expensive value itself. That keeps the expensive construction logic from being duplicated across competing threads.

Enumeration Is Safe but Not Frozen

Enumerating a ConcurrentDictionary is thread-safe, but you should not treat the enumeration as a perfectly frozen snapshot of future state. Other threads may continue to add or remove entries while you iterate.

That means enumeration is good for:

  • diagnostics
  • metrics
  • approximate reporting

It is not a substitute for a transactional view of shared state.

Keys Must Stay Stable

Just like with a normal dictionary, key equality and hash code must remain stable for as long as the key lives in the collection. Mutable key objects are still a bad idea.

If a key's hash or equality behavior changes after insertion, lookup behavior becomes unpredictable no matter how thread-safe the dictionary implementation is.

Common Pitfalls

  • Using ContainsKey plus assignment instead of GetOrAdd or TryAdd.
  • Assuming value factories run exactly once under contention.
  • Treating enumeration as if it were a strict immutable snapshot.
  • Storing mutable objects as keys whose equality or hash code can change.
  • Adding extra outer locks that defeat the purpose of ConcurrentDictionary.

Summary

  • Use ConcurrentDictionary for shared key-value access under concurrency.
  • Prefer atomic methods such as GetOrAdd, TryAdd, TryUpdate, and AddOrUpdate.
  • Do not build manual check-then-act sequences around dictionary operations.
  • Keep value factories side-effect free because they may execute more than once.
  • Thread-safe access does not remove the need for clear state-management design.

Course illustration
Course illustration

All Rights Reserved.