Search code examples
c#multithreadingthread-safetylockingsemaphore

What are the advantages of using `lock` over `SemaphoreSlim`?


I'm late to the party, but I recently learned about SemaphoreSlim:

I used to use lock for synchronous locking, and a busy boolean for asynchronous locking. Now I just use SemaphoreSlim for everything.

private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);

private void DoStuff()
{
    semaphoreSlim.Wait();
    try
    {
        DoBlockingStuff();
    }
    finally
    {
        semaphoreSlim.Release();
    }
}

vs

private object locker = new object();

private void DoStuff()
{
    lock(locker)
    {
        DoBlockingStuff();
    }
}

Are there any synchronous cases where I should prefer using lock over SemaphoreSlim? If so, what are they?


Solution

  • Here are the advantages of the lock over the SemaphoreSlim:

    1. The lock is reentrant, while the SemaphoreSlim is not. So programming with the lock is more forgiving. In case there is a rare path in your app where you are acquiring the same lock twice, the lock will acquire it successfully, while the SemaphoreSlim will deadlock.

    2. The lock is syntactic sugar around the Monitor class. In other words there is language support for the Monitor in C#, and there isn't for the SemaphoreSlim. So using the lock is comparatively more convenient and less verbose.

    3. You can write more robust code with the lock, because you can add debugging assertions in auxiliary methods that the lock has been acquired: Debug.Assert(Monitor.IsEntered(_locker));

    4. You can get contention statistics with the Monitor.LockContentionCount property: "Gets the number of times there was contention when trying to take the monitor's lock." There are no statistics available for the SemaphoreSlim class.

    5. The SemaphoreSlim is IDisposable, so you have to think about when (and whether) to dispose it. Can you get away without disposing it? Are you disposing it prematurely and risk an ObjectDisposedException? These are questions that you don't have to answer with the lock.

    6. The lock can survive in the scenario of an aborted thread. It is translated by the C# compiler like this:

        bool lockTaken = false;
        try
        {
            Monitor.Enter(obj, ref lockTaken);
            /* Here goes the code inside the `lock`. */
        }
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(obj);
            }
        }
    

    The Monitor.Enter has been coded carefully so that in case the thread is aborted, the lockTaken will have the correct value. On the contrary the SemaphoreSlim.Wait is typically called outside of the try/finally block, so there is a small window that the current thread can be aborted without releasing the lock, resulting in a deadlock.

    The .NET Core/5+ platform has dropped support for the Thread.Abort method, so you could rightfully say that the last point has only theoretical value.