Introduction
Given some implementation — e.g. through an interface (like a collection) — where "IsSynchronized
" defines the implementation type — the implementation must either be synchronized or not — this simple pattern inserts either a "real" lock, or a no-op.
The result is a single implementation, with either synchronized or un-synchronized behavior.
The lock is abstracted into an interface; and at this point, it can even be instrumented: the actual lock implementation can provide internal checking, and even change over time in the single implemented instance.
The given example is simple: the implementation needs a simple "block" locking scheme; but, the lock interface could be redifined as you need, and you could implement other types of locking, including reentrant choices, exit-and-reenter, try enter, etc.
Using the Code
The example use case for the pattern is, again, providing an implementation for an interface that may be implemented as either "synchronized" or not. The single implementation always inserts a lock; and in this simple example, that will simply be a no-op for the non-synchronized implementation.
For example, this interface:
public interface IBag
{
bool IsSynchronized { get; }
void Invoke();
}
The interface specifies either IsSynchronized
or not; and here we can implement both with a single implementation:
public class Bag
{
private readonly ISyncLock syncLock;
public Bag(bool isSynchronized)
{
syncLock = isSynchronized
? new SyncLock()
: NoOpSyncLock.Instance;
IsSynchronized = isSynchronized;
}
public bool IsSynchronized { get; }
public void Invoke()
{
using (syncLock.Enter()) {
}
}
}
The simple implementation always invokes a lock in the critical section, but the selected lock may in fact just be a no-op.
This simple "block" synchronization is just one example: the lock requirements maybe more complex, yet still, the lock implementation can be inserted through the single interface at all times, and just perform a no-op, OR also be changed or instrumented to alter the implementation. If possible, you are now able to push the locking requirements into the lock class and let your implementation just follow the pattern.
As illustrated in the sample code, the lock object could even be shared among internal classes that can participate in that implementation.
And so ... the implementation is IOC, and DRY.
The ISyncLock
implementation is like this:
public interface ISyncLock
{
IDisposable Enter();
}
public sealed class SyncLock
: ISyncLock,
IDisposable
{
private readonly object syncLock;
public SyncLock(object syncLock)
=> this.syncLock = syncLock ?? this;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDisposable Enter()
{
Monitor.Enter(syncLock);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
=> Monitor.Exit(syncLock);
}
public sealed class NoOpSyncLock
: ISyncLock,
IDisposable
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IDisposable Enter()
=> this;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose() { }
}
The NoOpSyncLock
should be very cheap.
Notice that you do not lock the ISyncLock
object; and you do not Dispose that directly.
Use the Enter
method to get the correct IDisposable
.
That detail ensures correct encapsulation.
That's also why the ISyncLock
interface does not extend IDisposable
.
... And you can augment or modify the interface, in addition to providing different implementations.
Expanding
Notice that the lock implementation can be instrumented. As is, if you want to enter using TryEnter
and optionally throw exceptions, you can change that in a single place:
public IDisposable Enter()
{
if (Monitor.TryEnter(syncLock, myTimeout))
return this;
throw new TimeoutException("Failed to acquire the lock.");
}
And you are free to augment the interface with a TryEnter
method:
public IDisposable TryEnter(out bool gotLock)
{
if (Monitor.TryEnter(syncLock, myTimeout)) {
gotLock = true;
return this;
}
gotLock = false;
return NoOpSyncLock.Instance;
}
And then, your implementation must check the out gotLock
result; and do nothing unsafe — the No-Op lock would simply always return true.
You could define the interface as your needs require; and follow the same logic in your implementation.
Further implementations are possible. The using
idiom can also allow you to exit and reenter the lock safely in the same block — e.g., an implementation can use a counter, and/or a token when entered; and the Dispose
method can safely exit every time. An async
lock could also be implemented ... The point being that the lock interface provides the needed single pattern to wrap your required implementation; and can be swapped, and stubed out where the synchronization is not required, or, can not be implemented ... And as noted above, the single lock implementation can be shared among internal code that can implement it.
Points of Interest
- The lock implementation can be instrumented, and changed globally.
- The complete implementation selector is implemented in a single class ... DRY.
- The implementation is now using Inversion Of Control.
- The runtime cost for the no-op should be very cheap.
Attached Code
There is a simple sample attached with a contrived use case; that illustrates the point.
History
- 30th August, 2018: Initial version