Building an async exclusive lock

Spread the love

The Monitor class is useful for establishing critical sections in your .NET code. However, this is of no use in asynchronous scenarios for at least two reasons:

  1. Monitor has thread affinity, meaning the same thread must acquire and release.
  2. Monitor blocks while waiting to acquire — not good for async code paths.

There is in fact one synchronization primitive in .NET that is thread-neutral and provides nonblocking behavior, SemaphoreSlim. It could be used as follows:

// Assumes initialization using "new SemaphoreSlim(1, 1)" to provide exclusive
// access, i.e. number of free slots == number of total slots == 1
private async Task DoExclusiveWorkAsync(SemaphoreSlim slimLock)
{
    await slimLock.WaitAsync();
    try
    {
        // . . . exclusive work here . . .
    }
    finally
    {
        slimLock.Release();
    }
}

As an educational exercise, I decided I wanted to build my own async exclusive lock. My lock has one bonus feature: it returns a Token that can be used to prevent someone who does not own the lock from releasing it. Typically, “It is the programmer’s responsibility to ensure that threads do not release the semaphore too many times.” The Token is a design fix to prevent this problem from ever occurring in any reasonable situation — at the expense of one additional object allocation.

So, putting on my async TDD hat, I got started with the minimal interface…

public class ExclusiveLock
{
    public ExclusiveLock()
    {
    }

    public Task<Token> AcquireAsync()
    {
        throw new NotImplementedException();
    }

    public void Release(Token token)
    {
        throw new NotImplementedException();
    }

    public abstract class Token
    {
        protected Token()
        {
        }
    }
}

…and forged ahead with the first unit test (shown after refactoring):

[Fact]
public void Acquire_completes_sync_then_release_succeeds()
{
    ExclusiveLock l = new ExclusiveLock();

    ExclusiveLock.Token token = AssertTaskCompleted(l.AcquireAsync());

    l.Release(token);
}

You can follow the commit history and watch the unit tests spring forth before your eyes. Given the simplicity of the interface, only six tests were needed to get to a complete (single-threaded) implementation:

Note above that I said single-threaded — again, TDD helps us get the logic right but is not typically suited for concurrency verification. In the next post, I will discuss an integration test which can give us more confidence in the threading aspects.

3 thoughts on “Building an async exclusive lock

    1. Brian Rogers Post author

      Interesting link. The problem with that sample is that it burns a thread for each waiter. So while it is “async,” it is not scalable.

  1. Pingback: Orchestrating race conditions | WriteAsync .NET

Leave a Reply

Your email address will not be published. Required fields are marked *