This class is an Async/Await wrapped Dictionary. Of course it doesn't technically implement IDictionary, but the functionality is basically the same as an IDictionary. It achieves similar functionality to ConcurrentDictionary but with async/await, and is non-blocking.
Note: please pay attention to the challenge. The challenge is to see if thread safety can be broken. There may not be a strong justification for the class's existence, but this is not the question. The question is: does it stand up to testing?
public class AsyncDictionary<TKey, TValue> : IAsyncDictionary<TKey, TValue>, IDisposable
{
#region Fields
private readonly IDictionary<TKey, TValue> _dictionary;
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
private bool disposedValue = false;
#endregion
#region Func
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>> ContainsKeyFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary.ContainsKey(keyValuePair.Key));
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>> ClearFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>>((dictionary, keyValuePair) =>
{
dictionary.Clear();
return Task.FromResult(true);
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<int>> GetCountFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<int>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary.Count);
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<ICollection<TValue>>> GetValuesFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<ICollection<TValue>>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary.Values);
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<ICollection<TKey>>> GetKeysFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<ICollection<TKey>>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary.Keys);
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>> AddFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>>((dictionary, keyValuePair) =>
{
dictionary.Add(keyValuePair);
return Task.FromResult(true);
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>> AddOrReplaceFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>>((dictionary, keyValuePair) =>
{
if (dictionary.ContainsKey(keyValuePair.Key))
{
dictionary[keyValuePair.Key] = keyValuePair.Value;
}
else
{
dictionary.Add(keyValuePair.Key, keyValuePair.Value);
}
return Task.FromResult(true);
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>> ContainsItemFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary.Contains(keyValuePair));
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>> RemoveFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary.Remove(keyValuePair));
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>> RemoveByKeyFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<bool>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary.Remove(keyValuePair.Key));
});
private static readonly Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<TValue>> GetValueFunc = new Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<TValue>>((dictionary, keyValuePair) =>
{
return Task.FromResult(dictionary[keyValuePair.Key]);
});
#endregion
#region Constructor
public AsyncDictionary()
{
//Note: the constructor overload to allow passing in a different Dictionary type has been removed to disallow unsynchronized access. It can be added if you're careful.
_dictionary = new Dictionary<TKey, TValue>();
}
/// <summary>
/// This overload is used in cases where a standard Dictionary isn't the right choice. Warning: accessing the Dictionary outside this class will break synchronization
/// </summary>
//public AsyncDictionary(IDictionary<TKey, TValue> dictionary)
//{
// _dictionary = dictionary;
//}
#endregion
#region Implementation
//Only when C# 8 comes!
//TODO: IEnumerator<KeyValuePair<T1, T2>> GetEnumerator()
//TODO: IEnumerator IEnumerable.GetEnumerator()
public Task<ICollection<TKey>> GetKeysAsync()
{
return CallSynchronizedAsync(GetKeysFunc, default);
}
public Task<ICollection<TValue>> GetValuesAsync()
{
return CallSynchronizedAsync(GetValuesFunc, default);
}
public Task<int> GetCountAsync()
{
return CallSynchronizedAsync(GetCountFunc, default);
}
public Task AddAsync(TKey key, TValue value)
{
return CallSynchronizedAsync(AddFunc, new KeyValuePair<TKey, TValue>(key, value));
}
public Task AddAsync(KeyValuePair<TKey, TValue> item)
{
return CallSynchronizedAsync(AddFunc, item);
}
public Task AddOrReplaceAsync(TKey key, TValue value)
{
return CallSynchronizedAsync(AddOrReplaceFunc, new KeyValuePair<TKey, TValue>(key, value));
}
public Task ClearAsync()
{
return CallSynchronizedAsync(ClearFunc, default);
}
public Task<bool> GetContainsAsync(KeyValuePair<TKey, TValue> item)
{
return CallSynchronizedAsync(ContainsItemFunc, item);
}
public Task<bool> GetContainsKeyAsync(TKey key)
{
return CallSynchronizedAsync(ContainsKeyFunc, new KeyValuePair<TKey, TValue>(key, default));
}
public Task<bool> RemoveAsync(TKey key)
{
return CallSynchronizedAsync(RemoveByKeyFunc, new KeyValuePair<TKey, TValue>(key, default));
}
public Task<bool> RemoveAsync(KeyValuePair<TKey, TValue> item)
{
return CallSynchronizedAsync(RemoveFunc, item);
}
public Task<TValue> GetValueAsync(TKey key)
{
return CallSynchronizedAsync(GetValueFunc, new KeyValuePair<TKey, TValue>(key, default));
}
#endregion
#region Private Methods
private async Task<TReturn> CallSynchronizedAsync<TReturn>(Func<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, Task<TReturn>> func, KeyValuePair<TKey, TValue> keyValuePair)
{
try
{
await _semaphoreSlim.WaitAsync();
return await Task.Run(async () =>
{
return await func(_dictionary, keyValuePair);
});
}
finally
{
_semaphoreSlim.Release();
}
}
#endregion
#region IDisposable Support
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_semaphoreSlim.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
I set up some some unit tests here, but I'd like to see if anyone can break the thread safety of this.
Can you add to the unit tests? Can you make the dictionary return the wrong results? Can you cause an exception that shouldn't come up from normal use of this class? Can you detect any other concurrency issues? Can find any other bugs
Note: PRs are more than welcome, and the more unit tests, the better!
public class AsyncDictionaryTests
{
#region Fields
private const int max = 800;
#endregion
#region Tests
[Test]
public async Task TestAddAndRetrieveKeys()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
await asyncDictionary.AddAsync(key, key.ToString());
var keys = (await asyncDictionary.GetKeysAsync()).ToList();
Assert.AreEqual(key, keys[0]);
}
[Test]
public async Task TestAddAndRetrieveValues()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
var value = key.ToString();
await asyncDictionary.AddAsync(key, value);
var values = (await asyncDictionary.GetValuesAsync()).ToList();
Assert.AreEqual(value, values[0].ToString());
}
[Test]
public async Task TestContainsKey()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
await asyncDictionary.AddAsync(key, key.ToString());
var contains = await asyncDictionary.GetContainsKeyAsync(key);
Assert.True(contains);
}
[Test]
public async Task TestContains()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
var value = key.ToString();
var kvp = new KeyValuePair<int, string>(key, value);
await asyncDictionary.AddAsync(kvp);
var contains = await asyncDictionary.GetContainsAsync(kvp);
Assert.True(contains);
}
[Test]
public async Task TestRemoveByKey()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
await asyncDictionary.AddAsync(key, key.ToString());
var contains = await asyncDictionary.GetContainsKeyAsync(key);
Assert.True(contains);
await asyncDictionary.RemoveAsync(key);
contains = await asyncDictionary.GetContainsKeyAsync(key);
Assert.False(contains);
}
[Test]
public async Task TestRemove()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
var kvp = new KeyValuePair<int, string>(key, key.ToString());
await asyncDictionary.AddAsync(kvp);
var contains = await asyncDictionary.GetContainsKeyAsync(key);
Assert.True(contains);
await asyncDictionary.RemoveAsync(kvp);
contains = await asyncDictionary.GetContainsKeyAsync(key);
Assert.False(contains);
}
[Test]
public async Task TestGetValue()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
await asyncDictionary.AddAsync(key, key.ToString());
var value = await asyncDictionary.GetValueAsync(key);
Assert.AreEqual(key.ToString(), value);
}
[Test]
public async Task TestClear()
{
var asyncDictionary = new AsyncDictionary<int, string>();
const int key = 1;
var value = key.ToString();
await asyncDictionary.AddAsync(key, value);
await asyncDictionary.ClearAsync();
var values = (await asyncDictionary.GetValuesAsync()).ToList();
Assert.IsEmpty(values);
}
[Test]
public async Task TestAnotherType()
{
var asyncDictionary = new AsyncDictionary<string, Thing>();
var thing = new Thing { Name="test", Size=100 };
await asyncDictionary.AddAsync(thing.Name, thing);
var newthing = await asyncDictionary.GetValueAsync(thing.Name);
Assert.True(ReferenceEquals(thing, newthing));
}
[Test]
public async Task TestThreadSafety()
{
var asyncDictionary = new AsyncDictionary<int, string>();
var tasks = new List<Task> { AddKeyValuePairsAsync(asyncDictionary), asyncDictionary.ClearAsync(), AddKeyValuePairsAsync(asyncDictionary) };
await Task.WhenAll(tasks);
tasks = new List<Task> { AddKeyValuePairsAsync(asyncDictionary), AddKeyValuePairsAsync(asyncDictionary), AddKeyValuePairsAsync(asyncDictionary) };
await Task.WhenAll(tasks);
tasks = new List<Task> { DoTestEquality(asyncDictionary), DoTestEquality(asyncDictionary), DoTestEquality(asyncDictionary), DoTestEquality(asyncDictionary), AddKeyValuePairsAsync(asyncDictionary) };
await Task.WhenAll(tasks);
}
#endregion
#region Helpers
private static async Task DoTestEquality(AsyncDictionary<int, string> asyncDictionary)
{
var tasks = new List<Task>();
for (var i = 0; i < max; i++)
{
tasks.Add(TestEquality(asyncDictionary, i));
}
await Task.WhenAll(tasks);
}
private static async Task TestEquality(AsyncDictionary<int, string> asyncDictionary, int i)
{
var expected = i.ToString();
var actual = await asyncDictionary.GetValueAsync(i);
Console.WriteLine($"Test Equality Expected: {expected} Actual: {actual}");
Assert.AreEqual(expected, actual);
}
private static async Task AddKeyValuePairsAsync(AsyncDictionary<int, string> asyncDictionary)
{
var tasks = AddSome(asyncDictionary);
await Task.WhenAll(tasks);
}
private static List<Task> AddSome(AsyncDictionary<int, string> asyncDictionary)
{
var tasks = new List<Task>();
for (var i = 0; i < max; i++)
{
tasks.Add(AddByNumber(asyncDictionary, i));
}
return tasks;
}
private static Task AddByNumber(AsyncDictionary<int, string> asyncDictionary, int i)
{
return asyncDictionary.AddOrReplaceAsync(i, i.ToString());
}
#endregion
}
To see a UWP sample application, please clone the repo and run the sample there.
Notes: this class is designed for: maintainability first, concurrency, and flexibility. It is modeled after IDictionary but embraces the async-await paradigm. It comes after years of frustration in trying to synchronise cache in async-await C# apps while trying to avoid blocking calls.
It is heavily based on SemaphoreSlim with a maximum request concurrency of 1. Experience seems to indicate that this class behaves in a FIFO manner. However, the notes on SemaphoreSlim are a little worrying:
If multiple threads are blocked, there is no guaranteed order, such as FIFO or LIFO, that controls when threads enter the semaphore.
Is this an Achilles heal? The SemaphoreSlim code can be found here.
Can you create a scenario where the FIFO is not honored in a way that breaks the functionality of the class?
Conclusion: the marked answer exploits a mistake in the original code to break thread safety. However, the exercise was informative, as the larger question arose: what would the point of this class be? From my naieve perspective, it's designed in such a way that beginner programmers could use it, and likely achieve success with thread safety which is a little less complex than using ConcurrentDictionary, and uses the async-await pattern. Would this approach be recommended? Certainly not for performance reasons. But, the question would need to be asked in a different way to determine whether this class is useful or not.