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?
Here are the advantages of the lock
over the SemaphoreSlim
:
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.
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.
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));
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.
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
.
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.