Is it reasonable to use a ConcurrentDictionary<String, SemaphoreSlim>
with thousands or even millions of entries to lock only on specific keys? That is, something like
private static readonly ConcurrentDictionary<String, SemaphoreSlim> _Locks = new();
...
var _Lock = _Locks.GetOrAdd(_Key, (_) => new SemaphoreSlim(1, 1));
await _Lock.WaitAsync();
try { ... } finally { _Lock.Release() }
My main concerns would be:
(_) => new SemaphoreSlim(1, 1)
potentially being called extra times such that there are SemaphoreSlims that are allocated but ultimately never used.Update with further context:
I reality, I probably only need to support between 1k - 10k entries.
I am trying to use the SemaphoreSlim
s to lock on updates to another ConcurrentDictionary that acts as a cache by the same key.
private static readonly ConcurrentDictionary<String, SemaphoreSlim>
_Locks = new();
private static readonly ConcurrentDictionary<String, ImmutableType> _Cache = new();
...
var _Value;
var _Lock = _Locks.GetOrAdd(_Key, (_) => new SemaphoreSlim(1, 1));
await _Lock.WaitAsync();
try
{
if(!_Cache.TryGetValue(_Key, out _Value) || _Value.ExpirationTime < DateTime.UtcNow)
{
//do expensive operation to construct the _Value
//possibly return from the method if we can't construct the _Value
//(we can't use a Lazy Task - we are in the middle of a bi-direction gRPC call on the server side)
_Cache[_Key] = _Value;
}
} finally { _Lock.Release() }
Note that the _Value
type is an immutable class, we are just trying to avoid blocking other callers for other keys while refreshing our cache for the key in question.
Also note that I am not worried about evicting stale entries. We refresh them as needed but never remove them.
Having a ConcurrentDictionary<K,V>
with millions of idle SemaphoreSlim
s sitting around is certainly concerning. It might not be a big deal if you have abundant memory available, but if you are aiming at economic use of resources it is possible to evict from the dictionary the SemaphoreSlim
s that are not actively used at the moment. It's not trivial because you have to track how many workers are using each semaphore, but it's not rocket science either. You can find implementations in this question:
If you are worried about SemaphoreSlim
s being left undisposed, see this question:
Disposing IDisposable
instances is the correct thing to do in principle, but practically the SemaphoreSlim.Dispose
method is a no-op, unless you are using the rarely used AvailableWaitHandle
property.