One of the things that make debugging threading easier is to reduce the amount of code that you are debugging. This makes syntactic sugar actually quite important when it comes to this problem space.
The lock keyword and sections are very valuable when it comes to this, but it falls short when it comes to more complex threading problems. The drawback is that the lock is always exclusive, there is no option for read & write locks. This in turn makes the read lock a bottleneck; but often worth it due to the simplicity.
The area where they start to fall short is in scenarios where there are many readers and only a few writers. In this scenario we would want to allow concurrent reads while locking writes are exclusive. This becomes even more important as people enjoy Linq and the foreach statements. Both of these would crash if the collection would change during the iteration of the collection.
The following code tries to get the best of both worlds, having the syntax of a lock and power of a more complex construct.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ReaderWriterLock { /// <summary> /// Wrapper for the ReaderWriterLockSlim /// </summary> public class ReaderWriterLockManager { private ReaderWriterLockSlim lockHost = new ReaderWriterLockSlim(); /// <summary> /// Use with a Using statement, While in the using you have a read lock /// </summary> /// public ReaderLock ReadLock() { return new ReaderLock(lockHost); } /// <summary> /// Use with a Using statement, while in the using you have a write lock /// </summary> /// public WriterLock WriteLock() { return new WriterLock(lockHost); } /// <summary> /// Use with a Using statement, while in the using you have a upgradable lock /// </summary> /// public UpgradableLock UpgradeLock() { return new UpgradableLock(lockHost); } /// <summary> /// Syntax helper for the ReaderWriterLockSlim /// </summary> public class ReaderLock : IDisposable { /// <summary> /// Reader Lock /// </summary> /// The ReaderWriterLockSlim public ReaderLock(ReaderWriterLockSlim host) { lockHost = host; lockHost.EnterReadLock(); } private ReaderWriterLockSlim lockHost = new ReaderWriterLockSlim(); /// <summary> /// IDisposable implementation /// </summary> public void Dispose() { lockHost.ExitReadLock(); } } /// <summary> /// Syntax helper for the ReaderWriterLockSlim /// </summary> public class WriterLock : IDisposable { /// <summary> /// Writer lock /// </summary> /// The ReaderWriterLockSlim public WriterLock(ReaderWriterLockSlim host) { lockHost = host; lockHost.EnterWriteLock(); } private ReaderWriterLockSlim lockHost = new ReaderWriterLockSlim(); /// <summary> /// IDisposable implementation /// </summary> public void Dispose() { lockHost.ExitWriteLock(); } } /// <summary> /// Syntax helper for the ReaderWriterLockSlim /// </summary> public class UpgradableLock : IDisposable { /// <summary> /// Creates an upgradable list /// </summary> /// The ReaderWriterLockSlim public UpgradableLock(ReaderWriterLockSlim host) { lockHost = host; lockHost.EnterUpgradeableReadLock(); } private ReaderWriterLockSlim lockHost = new ReaderWriterLockSlim(); private bool isUpgraded = false; /// <summary> /// Upgrade the lock /// </summary> public void Upgrade() { if (isUpgraded) return; lockHost.EnterWriteLock(); isUpgraded = true; } /// <summary> /// IDisposable implementation /// </summary> public void Dispose() { if (isUpgraded) lockHost.ExitWriteLock(); lockHost.ExitUpgradeableReadLock(); } } } }
A sample for a read / write scenario
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ReaderWriterLock { class SafeReporting { ReaderWriterLockManager dataLock = new ReaderWriterLockManager(); private Queue _data; void AddRecord(object record) { using (dataLock.WriteLock()) { _data.Enqueue(record); } } object GetRecord() { using (dataLock.WriteLock()) { return _data.Dequeue(); } } string RunQueueReport() { using (dataLock.ReadLock()) { return _data.Select(x=>x.ToString()).Aggregate((a, x) => a + ", " + x); } } } }
A sample for an upgrade scenario
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ReaderWriterLock { public class SimpleCache { ReaderWriterLockManager dataLock = new ReaderWriterLockManager(); private object _data; public object Data { get { using (var l = dataLock.UpgradeLock()) { if (_data == null) { l.Upgrade(); if (_data == null) { _data = new object(); } } return _data; } } } } }
There were a few design decisions that were made in here. First of the calls to enter the locks are methods. My original idea was to do it with properties to make it even cleaner; but this created an interesting bug. The bug will not actually manifest when you have the application run normally, but it does show up when you run it in debug mode. This happens when the IDE evaluates the property when you hover over it. The property creates a new lock, and the lock does not get disposed. The end result can cause some impressive head scratching.
The end result work well, and creates some nice clean code.