Making the Possible Impossible
Have you ever gone to a restraunt with a menu that was just too big? Every once and a while I wind up at a place with a huge menu and it takes forever to choose what kind of food I'm going to eat. That's not a problem when I go to a steak house. When I go to a steak house, I know exactly what I want. A rib-eye medium well with a side of mashed potatoes. Removing options always makes it easier to choose the right thing.
Programming is the same way. A lot of complexity in code exists only because we give ourselves too many options. For instance, consider the following code:
List<object> myList = new List<object>();
public void DoSomething()
{
foreach(object o in myList) { Console.WriteLine(o); }
}
public void DoSomethingElse()
{
myList.Add(new object());
}
This all looks fine and dandy, until two threads hit these methods at the same time and your app crashes. We can try to make this better like so:
public void DoSomething()
{
lock(myList)
{
foreach(object o in myList) { Console.WriteLine(o); }
}
}
public void DoSomethingElse()
{
lock(myList)
{
myList.Add(new object());
}
}
Which again works, but you always have to remember to lock the object. Some times you might forget. Why? Because you can. Nothing in the compiler and nothing in the .NET framework will tell you that you have to lock the object, it just assumes you are a threading wizard. There must be a better way... a way that we can gaurentee that we will never forget to lock our objects, but not make our code a complete mess at the same time. How about explicitly coding our intentions. Something like:
public void DoSomething()
{
myList.Read(list => {
foreach(object o in list) { Console.WriteLine(o); }
});
}
public void DoSomethingElse()
{
myList.Write(list => {
list.Add(new object());
}
}
Now, you might look at this code and say, "But I could still write to the list in the read block". Well, again, that is only if you give yourself that option. We can prevent that kind of situation completely with this little class I put together after reflecting on some comments from Eyal (http://blogs.microsoft.co.il/blogs/eyal/archive/2008/07/27/esb-on-iserviceoriented.aspx):
public interface ILockedObject<TRead, TWrite> where TWrite : TRead { void Read(Action<TRead> action); void Write(Action<TWrite> action); void Replace(ReplaceAction<TWrite> action); } public class ReaderWriterLockedObject<TRead, TWrite> : ILockedObject<TRead,TWrite> where TWrite : TRead { public ReaderWriterLockedObject() { } public ReaderWriterLockedObject(TWrite value) { _value = value; } TWrite _value; ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); public void Read(Action<TRead> action) { _rwLock.EnterReadLock(); try { action(_value); } finally { _rwLock.ExitReadLock(); } } public void Write(Action<TWrite> action) { _rwLock.EnterWriteLock(); try { action(_value); } finally { _rwLock.ExitWriteLock(); } } public void Replace(ReplaceAction<TWrite> action) { _rwLock.EnterWriteLock(); try { action(ref _value); } finally { _rwLock.EnterWriteLock(); } } } public delegate void ReplaceAction<T>(ref T value);
Now simply change your code to define the list as:
ILockedObject<IEnumerable<object>, IList<object>> _list = new ReaderWriterLockedObject<IEnumerable<object>, IList<object> >(new List<object>());
And it will be impossible for you to access the list without locking it and impossible for you to write to the list when you requested a read lock (ok, not impossible because you could make a bonehead move and cast it back to list or something, but it is much harder to mess up now). You'll still need to deal with other threading issues, but you'll have one less bad choice to worry about.