I've recently come across some areas of code in our solution that have a few thread-safety problems. Thread-safety is a minefield at best so I thought I'd have an attempt at writing a few little utility classes that would make it easier to write safe multi-threaded code.
The general idea is to link a resource with the lock(s) that protect it. If the resource is accessed outside of all of it's locks then a runtime exception is thrown.
Imagine a developer had written the following (example) caching object but had forgotten to take the _imageLock when clearing the cache.
Using the traditional .net locking mechanism (of just locking on an Object) we might corrupt the Dictionary if two threads call Add() and ClearCache() at the same time.
Using these utility classes, Padlock and Protected<T>, will instead throw an exception in the ClearCache() method because the Protected<T> object is being accessed outside of it's lock. This exception should surface during the developers normal testing procedure, before the code is even checked in.
public class SomeThreadSafeCache
{
Padlock _imageLock = new Padlock();
Protected<Dictionary<string, byte[]>> _imageCache;
public SomeThreadSafeCache()
{
//The variable _imageCache is associate with the _imageLock Padlock
//i.e. _imageCache is only available when we're locking on _imageLock.
_imageCache = new Protected<Dictionary<string, byte[]>>(
new Dictionary<string, byte[]>(),
_imageLock);
}
public void AddImage(string key, byte[] data)
{
using (_imageLock.Lock())
{
_imageCache.Value.Add(key, data);
}
}
public void ClearCache()
{
//Throws an exception because the protected
//resource is accessed outside of it's lock.
_imageCache.Value.Clear();
}
}
(I know that .net now comes with Concurrent collections which make my example pointless but the general idea of linking a resource with it's lock is what I'm trying to get at)
The Protected<T> class is able to be associated with as many Padlocks as you might want.
public class SomeOtherThreadSafeObject
{
Padlock _lock1 = new Padlock();
Padlock _lock2 = new Padlock();
Protected<int> _i;
public SomeOtherThreadSafeObject()
{
_i = new Protected<int>(0, _lock1, _lock2);
}
public void IncrementI()
{
using (_lock1.Lock())
{
using (_lock2.Lock())
{
_i.Value++;
}
}
}
}
(A trivial example for illustrative purposes only, the Interlocked.Increment function would do a better job.)
If either _lock1 or _lock2 is not taken then
_i.Value++
will throw an exception stating that the object is not fully locked.
Enforcing the order in which the locks are taken are outside the scope of the utility classes. I'm also trying to avoid the old ICollection.SyncRoot pattern by not allowing
Protected<T>.Padlock.Lock()
The locking policy itself should be up to the implementors of the class. All Protected<T> is interested is in is knowing if it's fully protected before allowing access to it's internal value.
What I'm asking for a review of is the general approach/idea. Does this seem like a sensible system? Are there any glaring problems with it that I've not thought of? If anyone wants to see the actual implementation code then I can post it on Github (or somewhere similar).
(I'm aware that this abuses the IDisposable/Using pattern but for added thread-safety I think it will be worth it for us).