Named lock list
Would you consider adding this to the library? Again, adapted from stephen cleary code found on stackoverflow and used regularly in my own libraries :)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Apex.TaskUtilities {
/// <summary>
/// Provides access to named asynchronous and synchronous locks.
/// IMPORTANT!! These locks are NOT re-entrant.
/// IMPORTANT!! Does not work cross-app-domain because SemaphoreSLIM is used internally.
/// </summary>
public sealed class AsyncLockList {
readonly Dictionary<object, SemaphoreReferenceCount> Semaphores = new Dictionary<object, SemaphoreReferenceCount>();
SemaphoreSlim GetOrCreateSemaphore(object key) {
lock (Semaphores) {
if (Semaphores.TryGetValue(key, out var item)) {
item.IncrementCount();
} else {
item = new SemaphoreReferenceCount();
Semaphores[key] = item;
}
return item.Semaphore;
}
}
/// <summary>
/// Synchronously blocks execution until the lock for the given key becomes available, and then blocks other
/// requests until the IDisposable has been disposed. Useage:
/// <code>
/// using (lockList.Lock("nameOfTheLock")) {
/// // do stuff inside lock
/// }
/// </code>
/// IMPORTANT!! These locks are NOT re-entrant.
/// IMPORTANT!! Does not work cross-app-domain because SemaphoreSLIM is used internally.
/// </summary>
/// <param name="key">A key to identify the requested lock.</param>
/// <returns>An object which must be disposed to release the lock.</returns>
public IDisposable Lock(object key) {
GetOrCreateSemaphore(key).Wait();
return new Releaser(Semaphores, key);
}
/// <summary>
/// Asynchronously blocks execution until the lock for the given key becomes available, and then blocks other
/// requests until the IDisposable has been disposed. Useage:
/// <code>
/// using (await lockList.LockAsync("nameOfTheLock")) {
/// // do stuff inside lock
/// }
/// </code>
/// IMPORTANT!! These locks are NOT re-entrant.
/// IMPORTANT!! Does not work cross-app-domain because SemaphoreSLIM is used internally.
/// </summary>
/// <param name="key">A key to identify the requested lock.</param>
/// <returns>An object which must be disposed to release the lock.</returns>
public async Task<IDisposable> LockAsync(object key) {
await GetOrCreateSemaphore(key).WaitAsync().ConfigureAwait(false);
return new Releaser(Semaphores, key);
}
sealed class SemaphoreReferenceCount {
public readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
public int Count { get; private set; } = 1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IncrementCount() => Count++;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DecrementCount() => Count--;
}
sealed class Releaser : IDisposable {
readonly Dictionary<object, SemaphoreReferenceCount> Semaphores;
readonly object Key;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Releaser(Dictionary<object, SemaphoreReferenceCount> semaphores, object key) {
Semaphores = semaphores;
Key = key;
}
public void Dispose() {
lock (Semaphores) {
var item = Semaphores[Key];
item.DecrementCount();
if (item.Count == 0)
Semaphores.Remove(Key);
item.Semaphore.Release();
}
}
}
}
}
I'll consider it, but any changes like this need to be made across the whole library - i.e., we'd also need a named AsyncSemaphore, named AsyncManualResetEvent, etc.
See also https://github.com/StephenCleary/AsyncEx/pull/16
I'm wondering if it would be possible to create some kind of naming wrapper?
I like this comment of yours from #16
Usually, when developers want a semaphore (or AsyncSemaphore) for each object, they just include it in the object itself
If I look over the code I wrote that uses the above object, I would probably find that I should have included an AsyncLock object in the context of the object being dealt with instead of creating a centralized repository of keyed locks.
Another reference-counted approach: https://stackoverflow.com/a/31194647/263693
-
Both of these approaches
lockthe whole ConcurrentDictionary to perform an atomic decrement-count-and-remove operation. I was wondering if this is really necessary? or could you just lock the RefCounted item instead, and useInterlocked.Decrement()andInterlocked.Increment()to safely change the reference count and remove the item? What am I missing? -
Also, why do you would use a SemaphoreSlim instead of a Monitor? Is this just for Async support?