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.
public sealed class ConcurrentSet<T> : System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
where T : notnullType 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.
public ConcurrentSet();ConcurrentSet(IEnumerable<T>) Constructor
Creates a set containing the distinct elements of items.
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<>.
public int Count { get; }Implements Count
Property Value
ConcurrentSet<T>.IsReadOnly Property
Gets a value indicating whether the System.Collections.Generic.ICollection<> is read-only.
public bool IsReadOnly { get; }Implements IsReadOnly
Property Value
Methods
ConcurrentSet<T>.Add(T) Method
Adds an item to the System.Collections.Generic.ICollection<>.
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<>.
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.
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.
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.
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<>.
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.
public bool TryAdd(T item);