The SynchronizedCollection<T>
is a synchronized List<T>
. It's a concept that can be devised in a second, and can be implemented fully in about one hour. Just wrap each method of a List<T>
inside a lock (this)
, and you are done. Now you have a thread-safe collection, that can cover all the needs of a multithreaded application. Except that it doesn't.
The shortcomings of the SynchronizedCollection<T>
become apparent as soon as you try to do anything non-trivial with it. Specifically as soon as you try to combine two or more methods of the collection for a conceptually singular operation. Then you realize that the operation is not atomic, and cannot be made atomic without resorting to explicit synchronization (locking on the SyncRoot
property of the collection), which undermines the whole purpose of the collection. Some examples:
- Ensure that the collection contains unique elements:
if (!collection.Contains(x)) collection.Add(x);
. This code ensures nothing. The inherent race condition between Contains
and Add
allows duplicates to occur.
- Ensure that the collection contains at most N elements:
if (collection.Count < N) collection.Add(x);
. The race condition between Count
and Add
allows more than N elements in the collection.
- Replace
"Foo"
with "Bar"
: int index = collection.IndexOf("Foo"); if (index >= 0) collection[index] = "Bar";
. When a thread reads the index
, its value is immediately stale. Another thread might change the collection in a way that the index
points to some other element, or it's out of range.
At this point you realize that multithreading is more demanding than what you originally thought. Adding a layer of synchronization around the API of an existing collection doesn't cut it. You need a collection that is designed from the ground up for multithreaded usage, and has an API that reflects this design. This was the motivation for the introduction of the concurrent collections in .NET Framework 4.0.
The concurrent collections, for example the ConcurrentQueue<T>
and the ConcurrentDictionary<K,V>
, are highly sophisticated components. They are orders of magnitude more sophisticated than the clumsy SynchronizedCollection<T>
. They are equipped with special atomic APIs that are well suited for multithreaded environments (TryDequeue
, GetOrAdd
, AddOrUpdate
etc), and also with implementations that aim at minimizing the contention under heavy usage. Internally they employ lock-free, low-lock and granular-lock techniques. Learning how to use these collections requires some study. They are not direct drop-in replacements of their non-concurrent counterparts.
Caution: the enumeration of a SynchronizedCollection<T>
is not synchronized. Getting an enumerator with GetEnumerator
is synchronized, but using the enumerator is not. So if one thread does a foreach (var item in collection)
while another thread mutates the collection in any way (Add
, Remove
etc), the behavior of the program is undefined. The safe way to enumerate a SynchronizedCollection<T>
is to get a snapshot of the collection, and then enumerate the snapshot. Getting a snapshot is not trivial, because it involves two method calls (the Count
getter and the CopyTo
), so explicit synchronization is required. Beware of the LINQ ToArray
operator, it's not thread-safe by itself. Below is a safe ToArraySafe
extension method for the SynchronizedCollection<T>
class:
/// <summary>Copies the elements of the collection to a new array.</summary>
public static T[] ToArraySafe<T>(this SynchronizedCollection<T> source)
{
ArgumentNullException.ThrowIfNull(source);
lock (source.SyncRoot)
{
T[] array = new T[source.Count];
source.CopyTo(array, 0);
return array;
}
}