Skip to content

Instantly share code, notes, and snippets.

@aaronhoffman
Created November 13, 2020 18:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aaronhoffman/fb9f354b7e0ccbb630d13fe6ec9c4d20 to your computer and use it in GitHub Desktop.
Save aaronhoffman/fb9f354b7e0ccbb630d13fe6ec9c4d20 to your computer and use it in GitHub Desktop.
Async Thread Safe Concurrent Dictionary
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
public class ThreadSafeConcurrentDictionary
{
public bool TryGetValue<TValue>(string key, out TValue value)
{
return _dictionary.TryGetValue(key, out value);
}
public TValue GetOrAdd<TValue>(string key, TValue value)
{
return GetOrAdd(key, () => value);
}
public TValue GetOrAdd<TValue>(string key, Func<TValue> valueFactory)
{
if (_dictionary.TryGetValue(key, out TValue value))
{
return value;
}
var lockObject = GetSemaphoreSlim(key);
lock (lockObject)
{
// double check after lock
if (_dictionary.TryGetValue(key, out value))
{
return value;
}
value = valueFactory();
_dictionary.TryAdd(key, value);
}
return value;
}
public async Task<TValue> GetOrAdd<TValue>(string key, Task<TValue> valueTask)
{
if (_dictionary.TryGetValue(key, out TValue value))
{
return value;
}
var semaphoreSlim = GetSemaphoreSlim(key);
// async lock
await semaphoreSlim.WaitAsync();
try
{
// double check after lock
if (_dictionary.TryGetValue(key, out value))
{
return value;
}
value = await valueTask;
_dictionary.TryAdd(key, value);
}
finally
{
semaphoreSlim.Release();
}
return value;
}
public TValue AddOrUpdate<TValue>(string key, TValue value)
{
return AddOrUpdate(key, () => value);
}
public TValue AddOrUpdate<TValue>(string key, Func<TValue> valueFactory)
{
var lockObject = GetSemaphoreSlim(key);
lock (lockObject)
{
var value = valueFactory();
var result = (TValue)_dictionary.AddOrUpdate(key, value, (existingKey, existingValue) => value);
return result;
}
}
public async Task<TValue> AddOrUpdate<TValue>(string key, Task<TValue> valueTask)
{
TValue result;
var semaphoreSlim = GetSemaphoreSlim(key);
// async lock
await semaphoreSlim.WaitAsync();
try
{
var value = await valueTask;
result = (TValue)_dictionary.AddOrUpdate(key, value, (existingKey, existingValue) => value);
}
finally
{
semaphoreSlim.Release();
}
return result;
}
public object Remove(string key)
{
var lockObject = GetSemaphoreSlim(key);
lock (lockObject)
{
_dictionary.TryRemove(key, out object value);
return value;
}
}
/// <summary>
/// Retrieve the SemaphoreSlim specific to this cache key.
/// This method is thread safe due to its use of Lazy<>.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
private SemaphoreSlim GetSemaphoreSlim(string key)
{
var lazySemaphoreSlim = _semaphoreSlimDictionary.GetOrAdd(key, x => new Lazy<SemaphoreSlim>(() => new SemaphoreSlim(1, 1)));
return lazySemaphoreSlim.Value;
}
private readonly ConcurrentDictionary<string, object> _dictionary = new ConcurrentDictionary<string, object>();
/// <summary>
/// `GetOrAdd` call on the dictionary is not thread safe and we might end up creating a SemaphoreSlim more than once.
/// To prevent this, Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
/// threads but only one of the objects succeeds in creating a SemaphoreSlim due to use of Lazy<>.Value.
/// </summary>
private readonly ConcurrentDictionary<string, Lazy<SemaphoreSlim>> _semaphoreSlimDictionary = new ConcurrentDictionary<string, Lazy<SemaphoreSlim>>();
}
@aaronhoffman
Copy link
Author

A Thread Safe ConcurrentDictionary that supports async locks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment