Over the last week I’ve been writing some locking code around a task queue, and wondering generally how to test whether locks are taken.
The straightforward thing to do is to take the locking calls as a seam and introduce a proxy, behind which I can add my own behaviour. I’ve not thought about the practicality of what followed once I started – it may be supremely impractical – but out of interest’s sake I thought I’d see where it went.
I’ve been working with ReaderWriterLockSlim a fair amount, so that was a natural place to start. I added a hook that meant I could introduce a new implementation for EnterWriteLock. There are a couple of wrinkles over the work I did with static classes recently – in this case I’m explicitly proxying a class instance, so I can’t rely on static hooks into external code, and default them with a static method group.
Neither can I specify a lambda, as it would rely on calling methods on an instance that wasn’t available at compile time, leading to errors like “cannot access non-static member in static context”. This just means I need to resolve any default implementation at runtime.
namespace Threading.Abstractions { public class ReaderWriterLockSlim { private readonly System.Threading.ReaderWriterLockSlim _lock = new System.Threading.ReaderWriterLockSlim(); public ReaderWriterLockSlim() { EnterWriteLockImpl = (Action)(() => _lock.EnterWriteLock()); } public void EnterWriteLock() { EnterWriteLockImpl(); } public Action EnterWriteLockImpl; } }Then in a test I might replace the EnterWriteLock implementation:
[TestFixture] public class Tests { [Test] public void SimpleLockTest() { var locker = new Threading.Abstractions.ReaderWriterLockSlim(); var count = 0; locker.EnterWriteLockImpl = (Action)(() => count++); locker.EnterWriteLock(); Assert.AreEqual(1, count); } }
So, that's good, I can tell if the lock has been taken or not. I can take it a little bit further though. One thing I might want to do is check log which locks had been taken when – something that might notionally be useful even in production code. If I add an OnEnterWriteLock hook then I can log an entry if needed:
namespace Threading.Abstractions { public class ReaderWriterLockSlim { private readonly System.Threading.ReaderWriterLockSlim _lock = new System.Threading.ReaderWriterLockSlim(); public ReaderWriterLockSlim() { EnterWriteLockImpl = (Action)(() => _lock.EnterWriteLock()); } public Action OnEnterWriteLock; public Action AfterEnterWriteLock; public void EnterWriteLock() { if(OnEnterWriteLock != null) OnEnterWriteLock(); EnterWriteLockImpl(); if (AfterEnterWriteLock != null) AfterEnterWriteLock(); } public Action EnterWriteLockImpl; } }
After that I can add any logging that I might want to – I could append part of the stack trace for example; the name of the object that’s being called; or any number of things that help us track how it’s used:
[TestFixture] public class Tests { [Test] public void LockTest() { var locker = new Threading.Abstractions.ReaderWriterLockSlim(); locker.OnEnterWriteLock = (Action) (() => Console.WriteLine("Entering write lock") ); locker.AfterEnterWriteLock = (Action)(() => Console.WriteLine("Entered write lock")); locker.EnterWriteLock(); } }
Another interesting thing to try might be to actually create a timeline of all locks being taken and released. If I create a simple lock log:
namespace Threading.Abstractions { public static class LockHistory { private static object locker = new object(); public readonly static System.Collections.Generic.ListEntries = new System.Collections.Generic.List (); public static void Add(string lockEvent) { lock (locker) Entries.Add(lockEvent); } } }
I can then add a log entry every time a lock is taken or released, and play that log back at the end of the test.
[TestFixture] public class Tests { [Test] public void LockTest() { var locker = new Threading.Abstractions.ReaderWriterLockSlim(); locker.OnEnterWriteLock = (Action) (() => Threading.Abstractions.LockHistory.Add("Entering Write Lock") ); locker.AfterEnterWriteLock = (Action)(() => Console.WriteLine("Entered write lock")); locker.EnterWriteLock(); foreach (var entry in Threading.Abstractions.LockHistory.Entries) { Console.WriteLine(entry); } } }
The log history could then be parsed or analysed in some way. All good fun, although, as I say -- I'm not sure how practical it might be.