Fredrik Mörk has an interesting post on using extension methods to hide housekeeping code related to protecting access to a share resource using a ReaderWriterLockSlim.
I recommend reading the article, but the short of it is that the standard try, lock, operate, finally, unlock process is moved to an extension method and the operate step is passed in at the call-site as a delegate using a lambda.
I like this method and this post is mostly just a jam session with Fredrik, I don't know if you'll prefer this method to his, but I did enough to write this post.
The general idea is to create a proxy object that implements IDisposable
that can be wrapped in a using statement; when created this proxy will take the lock, and when disposed it will release it. We're taking advantage of the using statement's guarantee that it will always call Dispose on its owned object when execution leaves its scope, whether by successful program flow or by a thrown exception.
First the proxy objects:
public class ReadLockProxy : IDisposable {
ReaderWriterLockSlim _rwlock;
public ReadLockProxy(ReaderWriterLockSlim rwlock) {
_rwlock = rwlock;
_rwlock.EnterReadLock();
}
public void Dispose() {
_rwlock.ExitReadLock();
}
}
public class WriteLockProxy : IDisposable {
ReaderWriterLockSlim _rwlock;
public WriteLockProxy(ReaderWriterLockSlim rwlock) {
_rwlock = rwlock;
_rwlock.EnterWriteLock();
}
public void Dispose() {
_rwlock.ExitWriteLock();
}
}
public class UpgradeableReadLockProxy : IDisposable {
ReaderWriterLockSlim _rwlock;
public UpgradeableReadLockProxy(ReaderWriterLockSlim rwlock) {
_rwlock = rwlock;
_rwlock.EnterUpgradeableReadLock();
}
public void Dispose() {
_rwlock.ExitUpgradeableReadLock();
}
}
These proxies are enough to make use of our pattern; a usage (to borrow Fredrik's example) would look like this:
private static int GetFromQueueWithProxies() {
int result = -1;
using(new UpgradeableReadLockProxy(_lock))
{
if (_sharedResource.Count > 0)
{
using(new WriteLockProxy(_lock))
{
result = _sharedResource.Dequeue();
}
}
}
return result;
}
This code is arguably a tad cleaner than passing a lambda into an extension method, but as far as mental workload is concerned, the new operator and passing in the lock object don't seem like an equitable trade-off. Lets also take advantage of some extension methods to make this code even more straight forward.
public static class LockExtensions {
public static ReadLockProxy UseReadLock(this ReaderWriterLockSlim rwlock) {
return new ReadLockProxy(rwlock);
}
public static WriteLockProxy UseWriteLock(this ReaderWriterLockSlim rwlock) {
return new WriteLockProxy(rwlock);
}
public static UpgradeableReadLockProxy UseUpgradeableReadLock(this ReaderWriterLockSlim rwlock) {
return new UpgradeableReadLockProxy(rwlock);
}
}
I think these are pretty self explanitory, they just wrap the creation of the proxies into extension methods; let's take a look at a revised example:
private static int GetFromQueueUsingExtensions() {
int result = -1;
using(_lock.UseUpgradeableReadLock())
{
if (_sharedResource.Count > 0)
{
using(_lock.UseWriteLock())
{
result = _sharedResource.Dequeue();
}
}
}
return result;
}
Ah, much better, we are able to make our code more readable and much more succinct by taking advantage of two C# language features, using statements and extension methods.
I hope you enjoyed this jam session, I know I did; big thanks to Fredrik for the initial riff!