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();
}
}