You may have noticed that the C# compiler gets very upset if you try to use an await inside a lock statement

object guard = new object();

lock (guard)
{
    string content = await GetContent();
}

results in a [CS1996] Cannot await in the body of a lock statement compiler error.

A lock statement is an exception safe wrapper around Monitor.Enter and Monitor.Exit. Monitor.Enter acquires a thread sensitive construct called a SyncBlock for the calling thread and Monitor.Exit relinquishes it. Therefore, Monitor.Enter and Monitor.Exit must be called on the same thread for the model to work correctly.

An await is used where the caller states that they know the call they are about to make may take some time and so prefers to give up the thread rather than block it. When the awaited call completes, the code after the await will execute but this may not be on same thread that called await. Therefore, the compiler cannot guarantee that the Monitor.Exit called on the closing brace of the lock statement will be on the same thread on which Monitor.Enter was called. This is the reason the compiler prevents the construct.

There is a problem with lock, you cannot pass a timeout. So if someone does not release the Monitor for some reason then a lock statement will wait forever trying to take ownership of the Monitor. However, there is another construct in C# that works in a similar try/finally way - the using statement. we can take advantage of this to create code, with the same semantics as lock, which takes a timeout using Monitor.TryEnter. Here is an implementation:

public class TimedLock
{
    private readonly object toLock;

    public TimedLock(object toLock)
    {
        this.toLock = toLock;
    }

    public LockReleaser Lock(TimeSpan timeout)
    {
        if (Monitor.TryEnter(toLock, timeout))
        {
            return new LockReleaser(toLock);
        }
        throw new TimeoutException();
    }

    public struct LockReleaser : IDisposable
    {
        private readonly object toRelease;

        public LockReleaser(object toRelease)
        {
            this.toRelease = toRelease;
        }
        public void Dispose()
        {
            Monitor.Exit(toRelease);
        }
    }
}

This can then be used as follows:

object guard = new object();

using(new TimedLock(guard).Lock(TimeSpan.FromSeconds(2)))
{
    // thread sensitive operations here
}

But there is an issue with this new "lock": the compiler has no idea what we are doing so will allow us to put an await into the using block. With our new TimedLock we are likely to get the following exception when we use it with an await.

System.Threading.SynchronizationLockException: Object synchronization method was called from an unsynchronized block of code

Using Monitor based synchronization is not going to work. We need a synchronization primitive that does not have thread affinity: enter SemaphoreSlim.

A semaphore is similar to a Monitor with a count. Many threads can acquire the semaphore, at the same time, up to a defined maximum. They are often used to control access to pools of resources and as signalling mechanisms for patterns like pub/sub. The key feature for this discussion is that one thread can acquire the semaphore and another release it (we'll also use another useful feature of SemaphoreSlim to wait to acquire it without blocking the thread) So our TimedLock code now becomes:

public class TimedLock
{
    private readonly SemaphoreSlim toLock;

    public TimedLock()
    {
        toLock = new SemaphoreSlim(1, 1);
    }

    public async Task<LockReleaser> Lock(TimeSpan timeout)
    {
        if(await toLock.WaitAsync(timeout))
        {
            return new LockReleaser(toLock);
        }
        throw new TimeoutException();
    }

    public struct LockReleaser : IDisposable
    {
        private readonly SemaphoreSlim toRelease;

        public LockReleaser(SemaphoreSlim toRelease)
        {
            this.toRelease = toRelease;
        }
        public void Dispose()
        {
            toRelease.Release();
        }
    }
}

Now we have a try/finally style lock, with a timeout, that can be used with await.

Unfortunately, nothing is ever free. Unlike lock our SemaphoreSlim based TimedLock is not re-entrant. In other words, with lock a thread reacquiring a Monitor that it already holds will succeed (the thread must still exit the Monitor the right number of times). With our TimedLock, if the thread tries to acquire the lock for a second time, and the Semaphore is full, then it will block and the thread will, effectively, lock itself out.

Although it is not perfect, this TimedLock is a useful addition to your synchronization toolbox.

About the author