Building Async Coordination Primitives, Part 6: AsyncLock

Building Async Coordination Primitives, Part 6: AsyncLock

Rate This
  • Comments 18

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 8 and 7 and type the answer here:
  • Post
  • It was a bit strange for me that the sample code did not contain any await keyword, but it seems it is just a typo:

    using(var releaser = await m_lock.LockAsync())

    {

       … // protected code here

    }

    This should be work too:

    using(await m_lock.LockAsync())

    {

       … // protected code here

    }

  • Tamas, that doesn't parse correctly in the Developer Preview. You need to do using((await m_lock.LockAsync())).

  • Tamas, thanks, the missing await was just a typo... I've fixed it.

    And Cory is correct.  The reason I have the "var releaser =" in there is because there's a bug in the Visual Studio 11 Developer Preview that prevents "using(await ...)" from compiling.  Two workarounds are either the one I used, "using(var someVariable = await ...)", or the one Cory suggests, "using((await ...))".

  • Stephen, is there a point to making the IDisposable a struct? I think boxing would have slightly more overhead than simply instantiating a class right from the start.

  • Hi Cory-

    The point is to avoid that allocation entirely, i.e. nothing here should get boxed.  The struct is a public type, and the compiler-generated code for the using will make a constrained call to the Dispose method rather than first boxing the value into an IDisposable and making an interface-based call on the allocated object.  This is similar to how types like List<T> expose a struct-based Enumerator<T>, which foreach then makes calls to without boxing the enumerator.

  • Interesting, I did not know using() behaved like that!

  • Hello, Stephen!

    Sorry for question about old topic. What's about contract of Dispose() method, that says "If an object's Dispose method is called more than once, the object must ignore all calls after the first one." (msdn.microsoft.com/.../system.idisposable.dispose.aspx). As can I view, the Releaser::Dispose() violates it and second call of Dispose will change the state of an internal semaphore.

  • Hi Viacheslav-

    Thanks for the question. Yes, if this were a type to be publicly exposed from a production library, we'd want to consider that behavior (though it would add non-negligable overhead in order to catch erroneous usage)...but this is a blog post ;)

  • Thanks for your blog posts!

    Can I download the source of the "Building Async Coordination Primitives" series somewhere?

  • Peter, I don't currently have the source collected anywhere for download; right now you'll just need to copy it out of the posts.  Thanks for the interest.

  • One important point is that this class does not support reentrancy the way the Monitor class (or C# lock keyword) does.  So in that respect it's more like an AsyncSemaphore that's fixed at concurrency level 1.  Since this class returns an IDisposable releaser, I prefer this class to the last post's semaphore class.

  • How you would use the AsyncLock for synchronous calls. Say you have a class that has both synchronous and asynchronous methods and you want to protect the access to a socket for example. With asynchronous methods, you would wrap your method logic with

    using(await m_lock.LockAsync())

    {

       ...

    }

    but how would you wrap the corresponding synchronous method that accesses the same socket?

  • @Guy Godin: .NET 4.5 includes both synchronous and asynchronous Wait / WaitAsync methods on SemaphoreSlim.  You could implement this AsyncLock type around that instead, and then expose synchronous Lock methods that use the semaphore's synchronous Wait methods rather than asynchronous LockAsync methods that use the semaphore's asynchronous Wait methods.  You could also just block on the task returned from LockAsync.

  • Are there times in which using a traditional monitor based lock is better for performance and scalability than an AsyncLock? For example a very short lock in a cpu intensive code block?

  • @Bar Arnon: Yes, e.g. the scenario you mention. "Measure, measure, measure" applies here.

Page 1 of 2 (18 items) 12