Introduction
There are plenty of custom implementations of thread safe observable collections. Some are more advanced than others but in most scenarios these are overkill. The overhead introduced by concurrency code in most cases is not necessary. I will demonstrate how the regular ObservableCollection
could be used in a multithreaded environment without any need for creating a new type.
Background
In order to use a collection in a thread safe manner, at the very least, there has to be an object which could be used to synchronize access to the collection. Most custom solutions subclass the ObservableCollection
in order to associate a sync object with the collection but it is not necessary. ObservableCollection
already has that object. It derives from the Collection
class which implements the ICollection
interface.
public interface ICollection : IEnumerable
{
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
void CopyTo(Array array, int index);
}
The SyncRoot
property gives us exactly the object we need. Now synchronization of the collection is only a matter of properly applying locks only where and when it is required. There is no point in wasting any processing time where it is not needed.
Using the code
Using the collections in a thread safe manner is very simple. Just cast it into ICollection
and lock on the SyncRoot
property:
lock ((_collection as ICollection).SyncRoot)
{
_collection.Add(newObject);
}
SyncRoot
provides a universal way of accessing the synchronization object of the collection. So if you subclass a collection and create your own type, you will still be able to access and use this object and work in concert with whatever synchronization the class provides.
Implementing Concurrency in a derived type
If you wish to implement a thread safe collection as a derived type, it is still beneficial to use the SyncRoot
property. It gives you access to the same lock object inside and outside your class, and allows extra flexibility in handling non-trivial situations.
lock((this as ICollection).SyncRoot)
{
T removedItem = this[oldIndex];
base.RemoveItem(oldIndex);
base.InsertItem(newIndex, removedItem);
}
Because lock()
internally uses Monitor
, it is safe to call lock on the same thread recursively. This allows us to combine several operations under the same external lock:
lock ((_collection as ICollection).SyncRoot)
{
T removedItem = _collection[Index];
_collection.RemoveAt(oldIndex);
_collection.Insert(newIndex, removedItem);
}
When lock is applied to SyncRoot
inside RemoveAt
, Insert
, etc., it will not block. It will increase the reference counter and continue with the operation. Once the operation is done, it will decrease the counter and release the lock once all of them are done.
History
- 10/27/2011 - Released.
- 04/11/2012 - Recategorized as Tip