Another important issue is making sure currently held locks are always released even if an exception is thrown. ChainLocker does it for you by managing a list of held locks and wrapping the processing in try-finally block, where all held locks are released in the finally block. The hand-over-hand locking strategy of ChainLocker means that up to two locks may be held at any point in the processing. Here is what it looks like:
if (state0 == null)
{
throw new Exception("state0 can't be null");
}
var takenLocks = Enumerable.Repeat(false, 3).ToArray();
var states = Enumerable.Repeat<object>(null, 3).ToArray();
states[0] = state0;
try
{
...
}
finally
{
// Release still held locks (If an exception was thrown)
for (int i = 0; i < 3; ++i)
{
if (states[i] != null && takenLocks[i])
{
Monitor.Exit(states[i]);
}
}
}
(Please ignore the hard-coded number 3 in the code: This is generated code, so the DRY principle is not violated. When I write code manually, I don't hard-code the constant; but in generated code, it is acceptable and saves space.)
In the aforementioned code, the takenLocks variable is a Boolean flag array initialized to [false, false, false]. This means that, at this point, no lock is taken. The states variable is an array of objects that represent the objects that are locked/unlocked during processing. During normal processing, the locks will be released by ChainLocker in the hand-over-hand fashion; but if an exception is thrown, the finally block will unlock any remaining state objects that are still locked. It is crucial that the order in which the locks are taken by all threads is identical to avoid deadlocks.
Let's look at the actual processing done by ChainLocker inside the try block:
// Lock state0
Monitor.Enter(states[0], ref takenLocks[0]);
// Execute stage0
states[1] = stage0((T0)(states[0]));
// Bail out if returned null
if (states[1] == null)
{
return;
}
// Lock state1
Monitor.Enter(states[1], ref takenLocks[1]);
// Execute stage1
states[2] = stage1((T1)(states[1]));
// Bail out if returned null
if (states[2] == null)
{
return;
}
// Lock state2
Monitor.Enter(states[2], ref takenLocks[2]);
// Release the state1 lock so other threads can work on it
Monitor.Exit(states[1]);
takenLocks[1] = false;
// Execute stage2
stage2((T2)(states[2]));
The processing for each stage is similar (identical except that the last stage doesn't do an early bail out check). In each stage x, the corresponding state object states[x] is locked and the Boolean flag takenLocks[x] is set by Monitor.Enter(). Then, the stage x itself is executed (casting the state object to its actual Tx type), and the result is stored in states[x+1]. If the result is null, Do() simply returns (the finally block will clear the held lock).
Concrete ChainLocker Subclass
ChainLocker is great, but if you misuse it, you can enter a deadlock. Consider the following two methods using a two-level deep ChainLocker:
void Foo(A a)
{
ChainLocker<A, B>.Do(a, () => { return a.LookupChild() }, () => { ... });
}
void Bar(B b)
{
ChainLocker<B, A>.Do(b, () => { return b.Parent() }, () => { ... });
}
Assume the hierarchical data structure is "A contains a list of B objects." The Foo() method works in the order A -> B. But the Bar() method works in the order B -> A. If Foo() and Bar() are called from two separate threads, you may easily get into a deadlock where each method is trying to lock an object currently locked by the other thread. The generic ChainLocker will not protect you from this situation; but a trivial subclass/specialization can do it. Consider the following subclass for the MS:
public class SchoolLocker : ChainLocker<IDictionary<int, Lecture>, Lecture, Class>
{
}
Using SchoolLocker is less verbose than using the generic ChainLocker because you don't have to specify the parameterized types in each call and, of course, it guarantees that the locks are taken in the correct order.
public AttendLecture(int lectureId, int studentId)
{
SchoolLocker.Do(
_lectures,
lectures =>
{
Lecture lecture = null;
lectures.TryGetValue(lectureId, lecture);
return lecture;
},
lecture => lecture.FindAvailableClass(),
availableClass => availableClass.AddStudent(studentId));
}
Sharing State and Stage Isolation
In the AttendLecture() method shown in the last code snippet, I used the SchoolLocker subclass. It ensures only that the locks ChainLocker takes itself are taken in the right order. However, each stage delegate has access to its outer scope, and because it is defined inside a method, it also has access to the entire object state (including the root). This is very convenient if you want all stages to have access to some shared state or object, such as a logger. But it also allows the stage delegates to ignore all the nice machinations of the ChainLocker and potentially wreak havoc on the system. One way to minimize this risk is to define the stage delegates as static methods instead of in-place anonymous methods. This way, each delegate will have access only to the state variable passed to it by ChainLocker and to static variables of the hosting class. If you don't want to expose even static variables, you can define the delegates in a separate class altogether. Here is what it looks like:
private static Lecture LookupLecture(IDictionary<int, Lecture> lectures, int lectureId)
{
Lecture lecture = null;
lectures.TryGetValue(lectureId, out lecture);
return lecture;
}
private static Class FindAvailableClass(Lecture lecture)
{
return lecture.FindAvailableClass();
}
private static void AddStudent(Class availableClass, int studentId)
{
availableClass.AddStudent(studentId);
}
public void AttendLecture(int lectureId, int studentId)
{
new SchoolLocker().Do(
_lectures,
lectures => LookupLecture(lectures, lectureId),
lecture => FindAvailableClass(lecture),
availableClass => AddStudent(studentId));
}
Another benefit of this approach is that stages that are used by several methods (like LookupLecture()) can be reused. What about sharing something between all stages (like a logger) without exposing the entire outer scope? There is a special version of ChainLocker that accommodates this scenario. It's called StateLockerEx and has an extra argument called shared that is passed to each stage function. Here is ChainLockerEx for a two-level deep hierarchy:
public class ChainLockerEx<T, T0, T1>
where T0 : class
where T1 : class
{
T _sharedState;
public ChainLockerEx(T sharedState)
{
_sharedState = sharedState;
}
public void Do(T0 state0,
Func<T, T0, T1> stage0,
Action<T, T1> stage1)
{
if (state0 == null)
{
throw new Exception("state0 can't be null");
}
var takenLocks = Enumerable.Repeat(false, 2).ToArray();
var states = Enumerable.Repeat<object>(null, 2).ToArray();
states[0] = state0;
try
{
// Lock state0
Monitor.Enter(states[0], ref takenLocks[0]);
// Execute stage0
states[1] = stage0(_sharedState, (T0)(states[0]));
// Bail out if returned null
if (states[1] == null)
{
return;
}
// Lock state1
Monitor.Enter(states[1], ref takenLocks[1]);
// Release the state1 lock so other threads can work on it
Monitor.Exit(states[0]);
takenLocks[0] = false;
// Execute stage1
stage1(_sharedState, (T1)(states[1]));
}
finally
{
// Release still held locks (If an exception was thrown)
for (int i = 0; i < 2; ++i)
{
if (states[i] != null && takenLocks[i])
{
Monitor.Exit(states[i]);
}
}
}
}
}



