1

I recently had the need to add an asynchronous variant of the lock keyword to one of my applications. There are many implementations to choose from, but the two that most appealed to me were:

What are the differences during runtime for the two solutions?
Of course the AsyncEx AsyncLock has more features, but assuming I don't need those, how do the two compare?

cremor
  • 6,669
  • 1
  • 29
  • 72

1 Answers1

2

AFAIK the AsyncLock component of the AsyncEx library was authored before the introduction of the SemaphoreSlim.WaitAsync API, and the same is also true for Stephen Toub's AsyncLock synchronization primitive. On the contrary Scott Hanselman's AsyncLock was published after the release of the .NET Framework 4.5 (October 2012), and so it is making use of the aforementioned API. It is actually a very thin and lightweight wrapper around this API.

My assumption is that all these components have the same runtime behavior. It is hard to say it with absolute confidence for the AsyncEx version, because the source code is quite complex, and it's not contained in a single code file.

My personal preference between those options would be to use none. I find using a SemaphoreSlim(1, 1) directly quite convenient, and I am not a fan of (ab)using the using statement for purposes other than releasing unmanaged resources.

await mutex.WaitAsync();
try
{
    //...
}
finally { mutex.Release(); }

It's not much more code than:

using (await mutex.LockAsync())
{
    //...
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • > (ab)using the using statement for purposes other than releasing unmanaged resources. This is not an abuse of using. Using just ensures Dispose() is called, which doesn't necessarily have anything to do with unmanaged resources. – Ryan Jul 13 '23 at 03:48
  • @Ryan according to the [documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/using) *"The `using` statement ensures the correct use of an `IDisposable` instance"*. Also according to the [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable) the `IDisposable` *"Provides a mechanism for releasing unmanaged resources"*. I am not making this up. Releasing unmanaged resources is the stated intended purpose of this interface. – Theodor Zoulias Jul 14 '23 at 06:28
  • These are sort of halve-truths though. The using statement *does* ensure the correct use of an IDisposable instance, but from the language spec, it only ensures that .Dispose() is called via duck typing. There are many "disposable" things in the framework that don't implement IDisposable for performance reasons. The documentation for IDisposable is also notoriously bad. Only the IDisposable pattern (with finalizer) is employed for disposing unmanaged resources. In their own documentation they have an example Dispose method that does nothing but call Dispose on managed objects. – Ryan Jul 17 '23 at 08:13
  • @Ryan having a public `Dispose` method is not enough. An object in `using` has to be implicitly convertible to `IDisposable`, otherwise the compiler throws a [CS1674](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs1674) error. Do you know of any class in the standard .NET libraries that implements the `IDisposable`, and the `Dispose` does something else than disposing unmanaged resources? I don't know of any. If you also don't know of any, then I think the case is closed: Using the `using` for other purposes is against the C#/.NET designers' intentions. – Theodor Zoulias Jul 17 '23 at 12:12
  • @Thordor Classes like StreamWriter feel like obvious examples. StreamWriter.Dispose() exists to flush managed buffers to the stream, and doesn't necessarily even call the underlying Stream.Dispose(). Usescases like this demonstrate Dispose() implementing managed cleanup work which has nothing to do with unmanaged resources, in the framework. – Ryan Jul 25 '23 at 08:46
  • @Ryan good example. The `StreamWriter` derives from the `TextWriter`, and the `TextWriter.Dispose` is a `virtual` method that does nothing. My guess is that the .NET designers anticipated that at least some of the classes deriving from the `TextWriter` would hold unmanaged resources, and decided to make the base class `IDisposable` to clean them up. The documentation of the [`TextWriter.Dispose`](https://learn.microsoft.com/en-us/dotnet/api/system.io.textwriter.dispose) links to a document titled "Cleaning Up Unmanaged Resources". Do you have another example, that doesn't involve inheritance? – Theodor Zoulias Jul 25 '23 at 09:08
  • @Ryan one egregious example of `IDisposable` misuse that I know of, is the [`ICacheEntry`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.icacheentry) interface. The way you add a value in the `MemoryCache` is to call `cache.CreateEntry`, and then `Dispose` this `ICacheEntry` instance. WTF? The `MemoryCache` is equipped with convenience extensions methods that make this create-dispose pattern optional, but still... This API is not part of the .NET standard libraries though. I don't know who is the original author of this library. What was he thinking? – Theodor Zoulias Jul 25 '23 at 19:00