Building Async Coordination Primitives, Part 6: AsyncLock

Building Async Coordination Primitives, Part 6: AsyncLock

Rate This
  • Comments 22

Last time, we looked at building an AsyncSemaphore.  Here, we’ll look at building support for an async mutual exclusion mechanism that supports scoping via ‘using’.

As mentioned in the previous post, semaphores are great for throttling and resource management.  You can give a semaphore an initial count of the number of things to protect, and then it’ll only allow that many consumers to successfully acquire the semaphore, forcing all others to wait until a resource is freed up and count on the semaphore is released.  That resource to protect could be the right to enter a particular region of code, and the count could be set to 1: in this way, you can use a semaphore to achieve mutual exclusion, e.g.

private readonly AsyncSemaphore m_lock = new AsyncSemaphore(1);

await m_lock.WaitAsync();
try
{
    … // protected code here

finally { m_lock.Release(); }

We could simplify this slightly by creating an AsyncLock type that supports interaction with the ‘using’ keyword.  Our goal is to be able to achieve the same thing as in the previous code snippet but instead via code like the following:

private readonly AsyncLock m_lock = new AsyncLock();

using(var releaser = await m_lock.LockAsync())
{
    … // protected code here
}

To achieve this, we’ll build the following type:

public class AsyncLock
{
    public AsyncLock();

    public Task<Releaser> LockAsync();

    public struct Releaser : IDisposable
    {
        public void Dispose();
    }
}

Internally, we’ll maintain two members.  We’ll use an AsyncSemaphore to handle the bulk of the logic.  We’ll also cache a Task<Releaser> instance to use when accesses to the lock are uncontended and thus we can avoid unnecessary allocations.

private readonly AsyncSemaphore m_semaphore;
private readonly Task<Releaser> m_releaser;

The Releaser is just an IDisposable implementation with a Dispose method that will call Release on the underlying semaphore.  This is what allows us to use the construct with ‘using’, such that the finally block generated by the ‘using’ will call Release on the semaphore just as we did in our hand-written example.

public struct Releaser : IDisposable
{
    private readonly AsyncLock m_toRelease;

    internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; }

    public void Dispose()
    {
        if (m_toRelease != null)
            m_toRelease.m_semaphore.Release();
    }
}

Our AsyncLock’s constructor will just initialize the members, creating a semaphore with an initial count of 1, and creating the cached releaser task with a releaser that points to this AsyncLock instance:

public AsyncLock()
{
    m_semaphore = new AsyncSemaphore(1);
    m_releaser = Task.FromResult(new Releaser(this));
}

And, finally, we need our Lock method.  We first call WaitAsync on the semaphore to get back a Task that represents our acquisition of the lock.  If the task is already completed, then we can synchronously return our cached Task<Releaser>; again, this means that if the lock is uncontended, there are no allocations.  If the wait task is not yet completed, then we return a continuation Task<Releaser> that will complete and hand back a new Releaser when the wait completes.

public Task<Releaser> LockAsync()
{
    var wait = m_semaphore.WaitAsync();
    return wait.IsCompleted ?
        m_releaser :
        wait.ContinueWith((_,state) => new Releaser((AsyncLock)state),
            this, CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}

Next time, we’ll step things up a bit and try our hand at implementing an asynchronous reader/writer lock.

Leave a Comment
  • Please add 1 and 1 and type the answer here:
  • Post
  • 2 part question: Are you the original author of the code above and can I use/modify it as part of a OS project? Is there licensing that will need to be added?

    I don't know much about licenses...

  • (my comment above was directed to Stephen Toub). Great article BTW!!!

  • @Stewart Anderson:  I wrote the code.  It's covered by the Microsoft Public License (opensource.org/.../ms-pl).  Glad you enjoyed the article.

  • I'm still a beginner in regards to Async & Await so correct me if I'm wrong, on the 'LockAsync' method, you could replace 'new Releaser((AsyncLock)state)' by 'm_releaser' without problems.

    Going even further, why not implementing 'LockAsync' method like the following?

    public async Task<Releaser> LockAsync()

    {

       await m_semaphore.WaitAsync();

       return m_releaser;

    }

  • Obviously, if we write the 'LockAsync' method in the way I described previously, we would need to change the type of 'm_releaser' to 'Releaser' instead of leaving it as a 'Task<Releaser>'.

    But I would also make other small changes.

    public class AsyncLock

    {

       readonly AsyncSemaphore m_semaphore;

       readonly Releaser m_releaser;

       public AsyncLock()

       {

           m_semaphore = new AsyncSemaphore(1);

           m_releaser = new Releaser(m_semaphore);

       }

       public async Task<IDisposable> LockAsync()

       {

           await  m_semaphore.WaitAsync();

           return m_releaser;

      }

       class Releaser : IDisposable

       {

           readonly AsyncSemaphore m_semaphore;

           public Releaser(AsyncSemaphore m_semaphore)

           {

                   this.m_semaphore = m_semaphore;

           }

           public void Dispose()

           {

                   m_semaphore.Release();

           }

       }

    }

    Note, since we are instantiating 'Releaser' just once, we don't need to leave it as a public 'struct' anymore.

  • @Daniel Bezerra: Functionally your suggestions are fine.  Performance-wise, though, they regress from what I have in this post.  The reason I cache the m_releaser is to avoid allocating the returned Task<Releaser> object in the case where the lock is available and the LockAsync method returns synchronously; using a cached Task<Releaser> makes that allocation-free, whereas making the method an async method and returning a Releaser instance forces the method to allocate a Task<Releaser> to store it.  Your second IDisposable suggestion forces another disposable object to be allocated, whereas by returning a struct we avoid that allocation.  I hope that helps.

  • @Stephen Toub: Now I see. I overlooked the inevitable instantiation of a task when using the 'async' keyword.

    I understand that method should avoid GC allocations. This is a place where I miss C++; the same type can be explicitly allocated on the stack or on the heap, with the added advantage of no worries about boxing neither about the Disposable pattern.

    It is sad, for us programmers, that more often than not we have to trade simplicity for efficiency.

    Anyway, thank you for your comments. They are really appreciated.

Page 2 of 2 (22 items) 12