This is my attempt at fixing issue (4) but keeping the spirit of the code you wrote by using 'lock (locker)'. As a more experienced developer can you check that what I have done makes sense and you are free to use this solution.
Also can you close issue (4), which seems to concluded.
Note: A senior dev found a problem with my code which I have now corrected, it should now be correct.
Thanks.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace TestLockProvider
{
public class LockProvider<T> where T : notnull
{
private readonly object locker = new();
readonly LazyConcurrentDictionary<T, InnerSemaphore> LockDictionary = new();
readonly InnerSemaphore _innerSemaphore = new(1, 1);
public LockProvider() { }
/// <summary>
/// Use lock to make it exclusive
/// </summary>
/// <param name="idToLock">the unique ID to perform the lock</param>
/// <returns></returns>
private InnerSemaphore GetOrAddExclusive(T idToLock)
{
InnerSemaphore semaphore;
if (!LockDictionary.TryGetValue(idToLock, out semaphore!))
{
semaphore = _innerSemaphore;
lock (locker)
{
LockDictionary.GetOrAdd(idToLock, semaphore);
}
}
return semaphore;
}
/// <summary>
/// Blocks the current thread (according to the given ID) until it can enter the LockProvider
/// </summary>
/// <param name="idToLock">the unique ID to perform the lock</param>
public void Wait(T idToLock)
{
InnerSemaphore semaphore;
semaphore = GetOrAddExclusive(idToLock);
semaphore.Wait();
}
/// <summary>
/// Asynchronously puts thread to wait (according to the given ID) until it can enter the LockProvider
/// </summary>
/// <param name="idToLock">the unique ID to perform the lock</param>
public async Task WaitAsync(T idToLock, CancellationToken cancellationToken = default)
{
InnerSemaphore semaphore;
semaphore = GetOrAddExclusive(idToLock);
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
}
public void Release(T idToUnlock)
{
if (LockDictionary.TryGetValue(idToUnlock, out var semaphore))
{
lock (locker)
{
semaphore!.Release();
if (!semaphore.HasWaiters && LockDictionary.TryRemove(idToUnlock, out semaphore!))
semaphore!.Dispose();
}
}
}
}
public class InnerSemaphore : IDisposable
{
private readonly SemaphoreSlim _semaphore;
private int _waiters;
public InnerSemaphore(int initialCount, int maxCount)
{
_semaphore = new SemaphoreSlim(initialCount, maxCount);
_waiters = 0;
}
public void Wait()
{
Interlocked.Increment(ref _waiters);
_semaphore.Wait();
}
public async Task WaitAsync(CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _waiters);
await _semaphore.WaitAsync(cancellationToken);
}
public void Release()
{
Interlocked.Decrement(ref _waiters);
_semaphore.Release();
}
public void Dispose()
{
_semaphore?.Dispose();
GC.SuppressFinalize(this);
}
public bool HasWaiters => _waiters > 0;
}
public class LazyConcurrentDictionary<TKey, TValue> where TKey : notnull
{
private readonly ConcurrentDictionary<TKey, Lazy<TValue>> _concurrentDictionary;
public LazyConcurrentDictionary()
{
_concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>();
}
public TValue GetOrAdd(TKey key, TValue value)
{
var lazyResult = _concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => value, LazyThreadSafetyMode.ExecutionAndPublication));
return lazyResult.Value;
}
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
var lazyResult = _concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication));
return lazyResult.Value;
}
public bool TryGetValue(TKey key, out TValue? value)
{
bool success = _concurrentDictionary.TryGetValue(key, out Lazy<TValue>? lazyResult);
value = success ? lazyResult!.Value : default;
return success;
}
public bool TryRemove(TKey key, out TValue? value)
{
var success = _concurrentDictionary.TryRemove(key, out Lazy<TValue>? lazyResult);
value = success ? lazyResult!.Value : default;
return success;
}
}
}