Skip to content

ConcurrentSet<T> Class

A set that several threads may add to and enumerate at the same time, losing neither what is already stored nor what is being added.

C#
public sealed class ConcurrentSet<T> : System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
    where T : notnull

Type parameters

T

The element type. Uses the type's default equality comparer.

Inheritance System.Object → ConcurrentSet<T>

Implements System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable

Remarks

The .NET base class library ships no concurrent set, so the choice is between a System.Collections.Generic.HashSet<> under a lock and the keys of a System.Collections.Concurrent.ConcurrentDictionary<>, which are exactly a concurrent set with a value nobody reads. The dictionary wins on two counts.

Its TryAdd is an atomic test-and-add that also reports which caller performed the addition. That is the operation the defect needed: if (!Contains) Add is two operations with a window between them, and a lock closes the window only for as long as every caller remembers to take it, whereas here the guarantee belongs to the type.

And its Keys property is documented to return "a copy of all the keys", not "kept in sync" with the dictionary. That promise is what makes this usable from an async loop: a reader holding a lock cannot await inside the loop, so any lock-based set would force the caller to copy first - and a copy is what this returns to begin with. A reader iterating while a writer adds therefore neither throws System.InvalidOperationException nor loses an element that was already there.

The alternative considered was an ImmutableHashSet field updated by System.Threading.Interlocked.CompareExchange<>(@@0@,@@0,@@0): correct, and lock-free, but it allocates a new version of the set per addition and spins a retry loop under contention, for no gain over this. The cost of what is here instead is a placeholder byte per element and a Count that takes every segment lock - both irrelevant at the sizes this holds.

Constructors

ConcurrentSet() Constructor

Creates an empty set.

C#
public ConcurrentSet();

ConcurrentSet(IEnumerable<T>) Constructor

Creates a set containing the distinct elements of items.

C#
public ConcurrentSet(System.Collections.Generic.IEnumerable<T> items);

Parameters

items System.Collections.Generic.IEnumerable<T>

Properties

ConcurrentSet<T>.Count Property

Gets the number of elements contained in the System.Collections.Generic.ICollection<>.

C#
public int Count { get; }

Implements Count

Property Value

System.Int32

ConcurrentSet<T>.IsReadOnly Property

Gets a value indicating whether the System.Collections.Generic.ICollection<> is read-only.

C#
public bool IsReadOnly { get; }

Implements IsReadOnly

Property Value

System.Boolean

Methods

ConcurrentSet<T>.Add(T) Method

Adds an item to the System.Collections.Generic.ICollection<>.

C#
public void Add(T item);

Parameters

item T

The object to add to the System.Collections.Generic.ICollection<>.

Implements Add(T)

Exceptions

System.NotSupportedException
The System.Collections.Generic.ICollection<> is read-only.

ConcurrentSet<T>.Clear() Method

Removes all items from the System.Collections.Generic.ICollection<>.

C#
public void Clear();

Implements Clear()

Exceptions

System.NotSupportedException
The System.Collections.Generic.ICollection<> is read-only.

ConcurrentSet<T>.Contains(T) Method

Determines whether the System.Collections.Generic.ICollection<> contains a specific value.

C#
public bool Contains(T item);

Parameters

item T

The object to locate in the System.Collections.Generic.ICollection<>.

Implements Contains(T)

Returns

System.Boolean
true if item is found in the System.Collections.Generic.ICollection<>; otherwise, false.

ConcurrentSet<T>.CopyTo(T[], int) Method

Copies the elements of the System.Collections.Generic.ICollection<> to an System.Array, starting at a particular System.Array index.

C#
public void CopyTo(T[] array, int arrayIndex);

Parameters

array T[]

The one-dimensional System.Array that is the destination of the elements copied from System.Collections.Generic.ICollection<>. The System.Array must have zero-based indexing.

arrayIndex System.Int32

The zero-based index in array at which copying begins.

Implements CopyTo(T[], int)

Exceptions

System.ArgumentNullException
array is null.

System.ArgumentOutOfRangeException
arrayIndex is less than 0.

System.ArgumentException
The number of elements in the source System.Collections.Generic.ICollection<> is greater than the available space from arrayIndex to the end of the destination array.

ConcurrentSet<T>.GetEnumerator() Method

Enumerates a snapshot taken when enumeration begins, so what a reader sees does not depend on what a writer does while it reads.

C#
public System.Collections.Generic.IEnumerator<T> GetEnumerator();

Implements GetEnumerator(), GetEnumerator()

Returns

System.Collections.Generic.IEnumerator<T>

Remarks

Goes through Keys deliberately, and not through the dictionary's own enumerator: the latter is documented as "safe to use concurrently with reads and writes ... however it does not represent a moment-in-time snapshot", so it would show some later additions and not others depending on timing. Neither form can throw, so this is not about safety - it is about a caller that enumerates across await points getting a defined answer rather than a race-dependent one. Do not "simplify" this to the dictionary's enumerator to save the copy.

ConcurrentSet<T>.Remove(T) Method

Removes the first occurrence of a specific object from the System.Collections.Generic.ICollection<>.

C#
public bool Remove(T item);

Parameters

item T

The object to remove from the System.Collections.Generic.ICollection<>.

Implements Remove(T)

Returns

System.Boolean
true if item was successfully removed from the System.Collections.Generic.ICollection<>; otherwise, false. This method also returns false if item is not found in the original System.Collections.Generic.ICollection<>.

Exceptions

System.NotSupportedException
The System.Collections.Generic.ICollection<> is read-only.

ConcurrentSet<T>.TryAdd(T) Method

Adds an item if it is not already present. Returns whether this call is the one that added it, which lets a caller act on the transition (persist, notify) exactly once even under concurrency - something System.Collections.Generic.ICollection<>.Add(@0) cannot express, since it returns nothing.

C#
public bool TryAdd(T item);

Parameters

item T

Returns

System.Boolean