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.List Entries =
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.
No comments:
Post a Comment