Concurrent HashSetT in .NET Framework?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
The .NET Framework has ConcurrentDictionary, ConcurrentQueue, ConcurrentBag, and other thread-safe collections, but it does not ship with a built-in ConcurrentHashSet<T>. If you need set semantics with concurrent access, the usual answer is to build on top of ConcurrentDictionary<TKey, TValue> or to protect a normal HashSet<T> with your own locking strategy.
The right choice depends on what operations you need. If your workload is mostly add, remove, and contains checks, a dictionary-backed set is usually the simplest and safest option.
Why There Is No Built-In ConcurrentHashSet<T>
A hash set is essentially a key-only hash table. ConcurrentDictionary<TKey, TValue> already solves most of the concurrency problem, so the framework did not add a separate concurrent set type in the same way it added queue or bag types.
That means a practical concurrent set can be modeled as:
- dictionary key = set item
- dictionary value = placeholder such as
byteorbool
The value is not meaningful. It just gives the dictionary something to store.
The Standard ConcurrentDictionary Approach
A minimal wrapper looks like this:
This gives you atomic add and remove behavior with thread-safe lookup.
Example Use from Multiple Threads
Because duplicate keys are ignored by TryAdd, the final count reflects unique items, not insertion attempts.
When a Lock Around HashSet<T> Is Better
A dictionary-backed set is not always the best design. If you need more complex multi-step operations or if several actions must be performed as one atomic block, a normal HashSet<T> with a lock can be easier to reason about.
This can be a better fit if the API needs stronger consistency guarantees than independent concurrent dictionary operations naturally provide.
Enumeration Semantics Matter
This is one area people often skip. Enumerating a concurrent collection while other threads are mutating it is not the same as taking a stable snapshot of a normal HashSet<T>.
With a ConcurrentDictionary-backed set, enumeration is thread-safe, but it may represent a moment-in-time view that does not reflect a single global lock-protected state across the entire iteration.
If your code needs a stable snapshot, copy the items first.
Or expose a snapshot method that materializes the current keys into a separate list or array.
Alternatives Beyond Mutable Concurrent Sets
Sometimes the best answer is neither a custom concurrent set nor a locked HashSet<T>. Other options include:
- '
ImmutableHashSet<T>with atomic reference replacement' - channel or queue-based ownership so only one thread mutates the set
- a normal
HashSet<T>used only on one actor or scheduler
Those designs can be simpler if the application already has a higher-level concurrency model.
Common Pitfalls
The biggest mistake is assuming HashSet<T> itself becomes safe just because reads are common and writes are rare. It does not. Another is wrapping ConcurrentDictionary but then exposing multi-step operations that are not truly atomic as a whole. Developers also often forget about enumeration behavior and assume a concurrent collection provides a perfectly stable iteration snapshot. Finally, if equality semantics matter, do not forget to pass the appropriate comparer into the underlying dictionary.
Summary
- .NET Framework does not include a built-in
ConcurrentHashSet<T>. - The usual implementation uses
ConcurrentDictionary<T, byte>as the backing store. - A locked
HashSet<T>can be better when you need stronger multi-step consistency. - Enumeration of concurrent collections is safe but not necessarily a stable snapshot.
- Choose the concurrency model that matches the operations you actually need, not just the collection name you wish existed.

