Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / threads

Abstract Lock Pattern To Selectively Synchronize An Implementation (IOC and DRY)

3.90/5 (7 votes)
30 Sep 2018CPOL3 min read 13.8K   40  
This illustrates a simple pattern that provides a lock that can always be invoked; and may be a no-op for a non-synchronized implementation.

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:

C#
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:

C#
public class Bag
{
    private readonly ISyncLock syncLock;


    /// <summary>
    /// Constructor.
    /// </summary>
    public Bag(bool isSynchronized)
    {
        syncLock = isSynchronized
                ? new SyncLock()
                : NoOpSyncLock.Instance;
        IsSynchronized = isSynchronized;
    }


    public bool IsSynchronized { get; }

    public void Invoke()
    {
        // Lock the selected implementation:
        using (syncLock.Enter()) {
            // Act ...
        }
    }
}

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:

C#
/// <summary>
/// Defines a Monitor that is entered explicitly, and
/// exited when a returned <see cref="IDisposable"/> object is Disposed.
/// </summary>
public interface ISyncLock
{
    /// <summary>
    /// Enters the Monitor. Dispose the result to exit.
    /// </summary>
    IDisposable Enter();
}

/// <summary>
/// Implements <see cref="ISyncLock"/> with <see cref="Monitor.Enter(object)"/>
/// and <see cref="Monitor.Exit"/>.
/// </summary>
public sealed class SyncLock
        : ISyncLock,
                IDisposable
{
    private readonly object syncLock;


    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="syncLock">Optional: if null, <see langword="this"/>
    /// is used.</param>
    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);
}

/// <summary>
/// Implements a do-nothing <see cref="ISyncLock"/>.
/// </summary>
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:

C#
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)