Both options are dangerous if we think about ThreadAbortException, an exception that can happen in older .NET Framework code although it's important to note that it won't occur in newer .NET Core code, as Microsoft say: "Even though this type exists in .NET Core and .NET 5+, since Abort is not supported, the common language runtime won't ever throw ThreadAbortException."
- Consider Option 1 and
ThreadAbortException happening between WaitAsync and try. In this case a semaphore lock would be acquired but never released. Eventually that would lead to a deadlock.
await _semaphore.WaitAsync();
// ThreadAbortException happens here
try
{
// todo
}
finally
{
_semaphore.Release();
}
- Now in Option 2, if
ThreadAbortException happens before a lock has been acquired, we'd still try to release somebody else's lock, or we'd get SemaphoreFullException if the semaphore is not locked.
try
{
// ThreadAbortException happens here
await _semaphore.WaitAsync();
// todo
}
finally
{
_semaphore.Release();
}
Theoretically, we can go with Option 2 and track whether a lock was actually acquired. For that we're going to put lock acquisition and tracking logic into another (inner) try-finally statement in a finally block. The reason is that ThreadAbortException doesn't interrupt finally block execution. So we'll have something like this:
var isTaken = false;
try
{
try
{
}
finally
{
await _semaphore.WaitAsync();
isTaken = true;
}
// todo
}
finally
{
if (isTaken)
{
_semaphore.Release();
}
}
Unfortunately, we're still not safe. The problem is that Thread.Abort locks the calling thread until the aborting thread leaves a protected region (the inner finally block in our scenario). That can lead to a deadlock. To avoid infinite or long-running semaphore awaiting we can interrupt it periodically and give ThreadAbortException a chance to interrupt execution. Now the logic feels safe.
var isTaken = false;
try
{
do
{
try
{
}
finally
{
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
}
}
while(!isTaken);
// todo
}
finally
{
if (isTaken)
{
_semaphore.Release();
}
}