Disclaimer: I'm the author of the NuGet package I mention here.
There have been several attempts at a recursive/reentrant async lock (some are listed below) but only one of them succeeds in providing all three of these at once:
- Asynchronicity
- Reentrance
- Mutual exclusion
As of this writing, the only correct implementation that I know of is:
https://www.nuget.org/packages/ReentrantAsyncLock/
The package documentation shows how to use it. Using it in your code would look like this:
public class Foo
{
ReentrantAsyncLock _lock = new ReentrantAsyncLock();
public async Task Bar()
{
await using (await _lock.LockAsync(CancellationToken.None))
{
await BarInternal();
}
}
public async Task BarInternal()
{
await using (await _lock.LockAsync(CancellationToken.None)) // No deadlock
{
// DO work
}
}
}
I'm sure everyone knows what asynchronicity is.
Your code is an example of reentrance.
This is an example of mutual exclusion:
var gate = new object();
var value = 0;
var tasks = new List<Task>();
for (var i = 0; i < 1000; i++)
{
var task = Task.Run(() =>
{
lock (gate)
{
value++; // Without the lock this is a race condition
}
});
tasks.Add(task);
}
Task.WhenAll(tasks).Wait();
Debug.Assert(value == 1000);
The regular lock
keyword in C# gives reentrance and mutual exclusion.
SemaphoreSlim
and a dozen other things give asynchronicity and mutual exclusion.
But it has been difficult for people to get all three together at once.
For example, Stephen Cleary linked to his proof of concept in his answer. But his fails these tests:
https://github.com/matthew-a-thomas/cs-reentrant-async-lock/blob/ece6e461c26f005da2122185cb9c5b884968f98a/ReentrantAsyncLock.Tests/ReentrantAsyncLockClass.cs
(Keep in mind those tests were originally written for the ReentrantAsyncLock
NuGet package, that's why some things are commented out that don't make sense for Stephen Cleary's RecursiveAsyncLock
, and that's why the test file has ReentrantAsyncLock
in its name. Compare that test file to the equivalent on the main
branch and you'll see what I mean.)
Of course he has never claimed that it would but has only ever cautioned people against using it. So this isn't a ding against Stephen. I'm just giving an example of how someone can make an async lock and at first glance it looks like it gives all three of the things I listed above when in fact it has trouble putting the second two things together.
Similar things can be said for all of these: