AsyncDictionary - Can you break thread safety?.Net caching service - thread safetyCountdownLatch Thread-Safety CheckGame stats storage / displayThread safety/Transaction enforcerReturn IEnumerable<KeyValuePair> from a private method; use Dictionary or anon. type?Simple and reusable system for user registration and tracking and auto-updates - follow-upThread-safety based on object valueGeneric parallel task queue returning an observable sequence of return valueAsync concurrency prevention class

Science fiction short story where aliens contact a drunk about Earth's impending destruction

Defense against attacks using dictionaries

Average period of peer review process

how do you harvest carrots in creative mode

Is "The life is beautiful" incorrect or just very non-idiomatic?

Custom division symbol

How can I unambiguously ask for a new user's "Display Name"?

Irish Snap: Variant Rules

Are there account age or level requirements for obtaining special research?

Is a player able to change alignment midway through an adventure?

Architectural feasibility of a tiered circular stone keep

What to say to a student who has failed?

Mathematical uses of string theory

Are there any music source codes for sound chips?

How to respectfully refuse to assist co-workers with IT issues?

for loop not working in bash

Did a flight controller ever answer Flight with a no-go?

In an emergency, how do I find and share my position?

Sun setting in East!

I got kicked out from graduate school in the past. How do I include this on my CV?

How do I request a longer than normal leave of absence period for my wedding?

Efficiently pathfinding many flocking enemies around obstacles

Non-visual Computers - thoughts?

Why were movies shot on film shot at 24 frames per second?



AsyncDictionary - Can you break thread safety?


.Net caching service - thread safetyCountdownLatch Thread-Safety CheckGame stats storage / displayThread safety/Transaction enforcerReturn IEnumerable<KeyValuePair> from a private method; use Dictionary or anon. type?Simple and reusable system for user registration and tracking and auto-updates - follow-upThread-safety based on object valueGeneric parallel task queue returning an observable sequence of return valueAsync concurrency prevention class






.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty margin-bottom:0;








9












$begingroup$


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?



Code Is Here



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!



Code Is Here



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.










share|improve this question











$endgroup$









  • 4




    $begingroup$
    Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers.
    $endgroup$
    – Mast
    Aug 12 at 10:02






  • 5




    $begingroup$
    Revision 9 of this question is under discussion on meta
    $endgroup$
    – Vogel612
    Aug 13 at 11:28










  • $begingroup$
    Additionally, comments about the question have been moved to chat
    $endgroup$
    – Vogel612
    Aug 13 at 11:34

















9












$begingroup$


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?



Code Is Here



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!



Code Is Here



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.










share|improve this question











$endgroup$









  • 4




    $begingroup$
    Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers.
    $endgroup$
    – Mast
    Aug 12 at 10:02






  • 5




    $begingroup$
    Revision 9 of this question is under discussion on meta
    $endgroup$
    – Vogel612
    Aug 13 at 11:28










  • $begingroup$
    Additionally, comments about the question have been moved to chat
    $endgroup$
    – Vogel612
    Aug 13 at 11:34













9












9








9


0



$begingroup$


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?



Code Is Here



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!



Code Is Here



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.










share|improve this question











$endgroup$




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?



Code Is Here



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!



Code Is Here



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.







c# unit-testing thread-safety concurrency async-await






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Aug 12 at 22:03







Melbourne Developer

















asked Aug 10 at 21:36









Melbourne DeveloperMelbourne Developer

1708 bronze badges




1708 bronze badges










  • 4




    $begingroup$
    Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers.
    $endgroup$
    – Mast
    Aug 12 at 10:02






  • 5




    $begingroup$
    Revision 9 of this question is under discussion on meta
    $endgroup$
    – Vogel612
    Aug 13 at 11:28










  • $begingroup$
    Additionally, comments about the question have been moved to chat
    $endgroup$
    – Vogel612
    Aug 13 at 11:34












  • 4




    $begingroup$
    Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers.
    $endgroup$
    – Mast
    Aug 12 at 10:02






  • 5




    $begingroup$
    Revision 9 of this question is under discussion on meta
    $endgroup$
    – Vogel612
    Aug 13 at 11:28










  • $begingroup$
    Additionally, comments about the question have been moved to chat
    $endgroup$
    – Vogel612
    Aug 13 at 11:34







4




4




$begingroup$
Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers.
$endgroup$
– Mast
Aug 12 at 10:02




$begingroup$
Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers.
$endgroup$
– Mast
Aug 12 at 10:02




5




5




$begingroup$
Revision 9 of this question is under discussion on meta
$endgroup$
– Vogel612
Aug 13 at 11:28




$begingroup$
Revision 9 of this question is under discussion on meta
$endgroup$
– Vogel612
Aug 13 at 11:28












$begingroup$
Additionally, comments about the question have been moved to chat
$endgroup$
– Vogel612
Aug 13 at 11:34




$begingroup$
Additionally, comments about the question have been moved to chat
$endgroup$
– Vogel612
Aug 13 at 11:34










4 Answers
4






active

oldest

votes


















9













$begingroup$

If you modify the AsyncDictionary while enumerating its keys/values it throws InvalidOperationException (if the backing dictionary is a Dictionary).



var numbers = new AsyncDictionary<int, int>();

foreach(var number in Enumerable.Range(1, 1000))

await numbers.AddAsync(number, number);


foreach(var number in await numbers.GetKeysAsync())

await numbers.RemoveAsync(number);



A ConcurrentDictionary handles this scenario just fine.






share|improve this answer









$endgroup$














  • $begingroup$
    Oooh. Nice one.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 9:55










  • $begingroup$
    I fixed the code with a ToList() . I also added your code @johnbot to the unit tests
    $endgroup$
    – Melbourne Developer
    Aug 12 at 10:00











  • $begingroup$
    No, adding ToList() at calling point won't make it thread-safe. Side question: while all those static methods are...fields?!
    $endgroup$
    – Adriano Repetti
    Aug 12 at 14:09










  • $begingroup$
    @Johnbot you found a glitch in the code and broke the thread safety. I believe you succeeded in the challenge. I was hoping to see if there were more fundamental issues that could break the thread safety, but you definitely achieved the aim.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 21:57


















24













$begingroup$

The reason why there is no async API for a dictionary is, that all operations on a dictionary are so fast, that there is no need for asynchronicity.



For concurrent scenarios there is the thread safe variant - the ConcurrentDictionary.



Adding an async API to these dictionaries has absolutely zero value. Rather it increases complexity and reduces performance.






share|improve this answer











$endgroup$










  • 2




    $begingroup$
    I'm starting to wonder if the above is true or not.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 5




    $begingroup$
    No developer is immune to getting lost in technical solutions that have no resistance at the end of the day. I think it is not even a question of experience / professionalism but rather a question of sharing ideas with colleagues / others. And even that may produce questionable solutions ;)
    $endgroup$
    – JanDotNet
    Aug 11 at 9:18






  • 1




    $begingroup$
    I think that what you have said, along with another answer are strong points that need to be taken in to consideration. I think to some extent, the question in my mind: "Why didn't the C# team create this?" has been answered to a large extent. But this wasn't the original challenge. The original challenge was to break the functionality of the class. The question of whether this class is useful or not is a separate question. I'm leaning toward thinking that it is not. However, IF it is not, I will explore that as a separate question.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:59






  • 1




    $begingroup$
    @MelbourneDeveloper: If you could batch up a bundle of work (like multiple key/value pairs to add), getting that all done while holding the lock once could be good for throughput vs. using atomic RMW operations for each one. (And bad for latency and maybe fairness if you aren't careful.) Or if you're getting false sharing with a concurrent hash table (threads on different cores modifying nearby entries), that could be worth trying to do something about. But inter-thread communication with callbacks is probably more expensive than inter-core latency for one cache miss.
    $endgroup$
    – Peter Cordes
    Aug 12 at 6:45






  • 1




    $begingroup$
    @MelbourneDeveloper - It is not efficient. The point of async is to wait on IO operations, because they are really, really slow (compared to the CPU anyway) and do not involve your thread. The idea is that during the IO operation your thread can do something else more worthwhile. Reading/writing to a dictionary however is pure memory shuffling, no IO whatsoever. And it all involves your thread. There's no moment when you could "wait" for something to happen in the background. Queueing some work to run in a background thread (Task.Run()) is purely artificial and unnecessary.
    $endgroup$
    – Vilx-
    Aug 12 at 16:14


















11













$begingroup$

It's pretty hard to break something that uses a global lock around everything. So this seems pretty thread-safe. But that doesn't answer the question of why you'd want to use this.



Asynchronous calls are useful for things that take a long time, particularly if you can delegate the "waiting" to some low-level event based solution (HTTP requests for example).



For anything else, they not only make for a very awkward use of the API, they also are rather slow and involve a lot of compiler generated machinery (which if you're using it for a long running task won't matter much, but for something as simple as adding to a dictionary is plain awful).



When optimising, it's always important to first figure out what you want to optimise (what scenarios are you particularly worried about? how many readers to writers? how much 'global' concurrency on the dictionary? how much races on existing keys?) and then to measure.



Just as a baseline here is the overhead your solution has when simply adding elements sequentially compared to a concurrent dictionary, the standard dictionary and the standard dictionary with a simple lock around it:



| Method | N | Mean | Error | StdDev | Ratio |
|------------------ |-------- |------------:|-----------:|-----------:|------:|
| AddSimple | 1000000 | 34.94 ms | 0.4827 ms | 0.4515 ms | 1.00 |
| | | | | | |
| AddSimpleWithLock | 1000000 | 52.14 ms | 0.7828 ms | 0.7323 ms | 1.00 |
| | | | | | |
| ConcurrentDic | 1000000 | 241.46 ms | 1.2975 ms | 1.2137 ms | 1.00 |
| | | | | | |
| AddAsync | 1000000 | 3,214.79 ms | 30.9326 ms | 28.9344 ms | 1.00 |


The following code was used to generate the results. If you haven't used BenchmarkDotNet before it's recommended to read up on it first to avoid getting incorrect results, but you can easily extend it for your other more interesting scenarios. In any case, the results are even worse than I imagined them to be (15 times slower than the concurrent dictionary is impressive even with all the boxing and async overhead - I guessed it'd be about 10 times at the start). (What shouldn't come as a surprise is how cheap uncontended locks are these days, everyone always running to ConcurrentDictionary and co might want to rethink that).



[ClrJob(baseline: true)]
public class DictionaryComparison

private Random _rand;

private Dictionary<int, int> _simpleDic;

private Dictionary<int, int> _simpleDicWithLock;

private readonly object _lock = new object();

private ConcurrentDictionary<int, int> _concurrentDic;

private AsyncDictionary<int, int> _asyncDictionary;

[Params( 1_000_000)]
public int N;

[IterationSetup]
public void IterationSetup()

_rand = new Random(0xdead);
_simpleDic = new Dictionary<int, int>();
_simpleDicWithLock = new Dictionary<int, int>();
_concurrentDic = new ConcurrentDictionary<int, int>();
_asyncDictionary = new AsyncDictionary<int, int>();


[Benchmark]
public void AddSimple()

for (int i = 0; i < N; i++)

_simpleDic[i] = i;



[Benchmark]
public void AddSimpleWithLock()

for (int i = 0; i < N; i++)

lock (_lock)

_simpleDicWithLock[i] = i;




[Benchmark]
public void ConcurrentDic()

for (int i = 0; i < N; i++)

_concurrentDic[i] = i;



[Benchmark]
public async Task AddAsync()

for (int i = 0; i < N; i++)

await _asyncDictionary.AddAsync(i, i);






public sealed class Program

private static async Task Main(string[] args)

var summary = BenchmarkRunner.Run<DictionaryComparison>();




PS: The work is so little even when adding a million elements, that the results are a bit dubious for the two AddSimple variants.






share|improve this answer











$endgroup$














  • $begingroup$
    This is very valuable information. I thank you for your time and rigour. I wasn't naive enough to think that this class would outperform ConcurrentDictionary. You've also introduced me to a useful technology BenchmarkDotNet and have made me question some of my original assumptions.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:50










  • $begingroup$
    What about performance of the async wrapper of ConcurrentDic I presented?
    $endgroup$
    – dfhwze
    Aug 12 at 3:56










  • $begingroup$
    @dfhwze The takeaway here should be to not introduce asynchronous wrapper APIs on top of synchronous APIs. Asynchronous code is mostly useful if you don't have to block a thread during the waiting process (http calls or timers are two classic examples). There simply is no benefit to be gained from wrapping here.
    $endgroup$
    – Voo
    Aug 12 at 4:32










  • $begingroup$
    I thought the whole point of this question was to have an async signature for dictionary operations :-(
    $endgroup$
    – dfhwze
    Aug 12 at 4:35






  • 4




    $begingroup$
    @dfhwze "They were so preoccupied with whether or not they could, they didn’t stop to think if they should." ;) It looks like a classical XY problem.
    $endgroup$
    – Voo
    Aug 12 at 6:05


















11













$begingroup$

Threading Design



Your implementation has a very intrusive lock for all read and write operations, using the SemaphoreSlim with max concurrency 1.




try

await _semaphoreSlim.WaitAsync()// <- both read/write operations acquire single mutex

return await Task.Run(async () =>

return await func(_dictionary, keyValuePair);
);

finally

_semaphoreSlim.Release();




CallSynchronizedAsync is hence thread-safe to the extend you introduce possible fairness issues. In .NET, multiple threads awaiting a lock to get signaled are not notified in a fair way, meaning thread B may get the lock before A even if A asked to acquire the lock before B.



ConcurrentDictionary mitigates the risk on unfair thread signaling by using the following behavior:



  • Read operations are volatile/atomic. No locks are used (Source: TryGetValue).

  • Write operations are optimized for fast access and minimum lock timespans (Source: TryAddInternal).

Perhaps you should wrap a ConcurrentDictionary with async/await Task.Run or Task.FromResult functionality to take advantage of the built-in locking mechanism.




API Design



After discussing a bit with you in the comments, it became apparent you would like normal dictionary method signatures. This should not be a problem. Let's say you have a private field of type concurrent dictionary _dictionary. One option is to call the code synchronously and return Task.CompletedTask to make it fit the async/await pattern.



public async Task AddAsync(TKey key, TValue value)

_dictionary.TryAdd(key, value);
return Task.CompletedTask;



You could decide to also provide async/await-aware methods with concurrent dictionary signatures:



public async Task<bool> TryAddAsync(TKey key, TValue value)

return Task.FromResult(_dictionary.TryAdd(key, value));






share|improve this answer











$endgroup$










  • 1




    $begingroup$
    This post explains thread fairness: stackoverflow.com/questions/4228864/….
    $endgroup$
    – dfhwze
    Aug 11 at 6:56







  • 2




    $begingroup$
    sorry, it looks like I was wrong. ConcurrentDictionary does implement IDictionary so I'm rethinking some of my assumptions. Have a look at the diff in this branch. The unit test still passes BTW: github.com/MelbourneDeveloper/AsyncDictionary/compare/…
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 2




    $begingroup$
    There's literally (yes I did look up the meaning I'm a dictionary once ;)) no reason to ever wrap a short-lived synchronous API call such as adding to a (concurrent) dictionary in Task.Run. That's simply bad design. On a slightly related note: Most people use ConcurrentDictionary in circumstances where a simple dictionary with lock would be faster - just goes to show how complicated performance matters can be.
    $endgroup$
    – Voo
    Aug 11 at 21:55






  • 1




    $begingroup$
    @Voo my example wraps in Task.FromResult, not Task.Run. Would that be good practice?
    $endgroup$
    – dfhwze
    Aug 12 at 4:21






  • 1




    $begingroup$
    @dfhwze Task.FromResult is there to allow synchronous implementations to implement asynchronous interfaces easily and with minimal overhead. Given that you control both implementation and interface there's no need for it either.
    $endgroup$
    – Voo
    Aug 12 at 6:03













Your Answer






StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");

StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);

else
createEditor();

);

function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);



);













draft saved

draft discarded


















StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f225911%2fasyncdictionary-can-you-break-thread-safety%23new-answer', 'question_page');

);

Post as a guest















Required, but never shown

























4 Answers
4






active

oldest

votes








4 Answers
4






active

oldest

votes









active

oldest

votes






active

oldest

votes









9













$begingroup$

If you modify the AsyncDictionary while enumerating its keys/values it throws InvalidOperationException (if the backing dictionary is a Dictionary).



var numbers = new AsyncDictionary<int, int>();

foreach(var number in Enumerable.Range(1, 1000))

await numbers.AddAsync(number, number);


foreach(var number in await numbers.GetKeysAsync())

await numbers.RemoveAsync(number);



A ConcurrentDictionary handles this scenario just fine.






share|improve this answer









$endgroup$














  • $begingroup$
    Oooh. Nice one.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 9:55










  • $begingroup$
    I fixed the code with a ToList() . I also added your code @johnbot to the unit tests
    $endgroup$
    – Melbourne Developer
    Aug 12 at 10:00











  • $begingroup$
    No, adding ToList() at calling point won't make it thread-safe. Side question: while all those static methods are...fields?!
    $endgroup$
    – Adriano Repetti
    Aug 12 at 14:09










  • $begingroup$
    @Johnbot you found a glitch in the code and broke the thread safety. I believe you succeeded in the challenge. I was hoping to see if there were more fundamental issues that could break the thread safety, but you definitely achieved the aim.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 21:57















9













$begingroup$

If you modify the AsyncDictionary while enumerating its keys/values it throws InvalidOperationException (if the backing dictionary is a Dictionary).



var numbers = new AsyncDictionary<int, int>();

foreach(var number in Enumerable.Range(1, 1000))

await numbers.AddAsync(number, number);


foreach(var number in await numbers.GetKeysAsync())

await numbers.RemoveAsync(number);



A ConcurrentDictionary handles this scenario just fine.






share|improve this answer









$endgroup$














  • $begingroup$
    Oooh. Nice one.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 9:55










  • $begingroup$
    I fixed the code with a ToList() . I also added your code @johnbot to the unit tests
    $endgroup$
    – Melbourne Developer
    Aug 12 at 10:00











  • $begingroup$
    No, adding ToList() at calling point won't make it thread-safe. Side question: while all those static methods are...fields?!
    $endgroup$
    – Adriano Repetti
    Aug 12 at 14:09










  • $begingroup$
    @Johnbot you found a glitch in the code and broke the thread safety. I believe you succeeded in the challenge. I was hoping to see if there were more fundamental issues that could break the thread safety, but you definitely achieved the aim.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 21:57













9














9










9







$begingroup$

If you modify the AsyncDictionary while enumerating its keys/values it throws InvalidOperationException (if the backing dictionary is a Dictionary).



var numbers = new AsyncDictionary<int, int>();

foreach(var number in Enumerable.Range(1, 1000))

await numbers.AddAsync(number, number);


foreach(var number in await numbers.GetKeysAsync())

await numbers.RemoveAsync(number);



A ConcurrentDictionary handles this scenario just fine.






share|improve this answer









$endgroup$



If you modify the AsyncDictionary while enumerating its keys/values it throws InvalidOperationException (if the backing dictionary is a Dictionary).



var numbers = new AsyncDictionary<int, int>();

foreach(var number in Enumerable.Range(1, 1000))

await numbers.AddAsync(number, number);


foreach(var number in await numbers.GetKeysAsync())

await numbers.RemoveAsync(number);



A ConcurrentDictionary handles this scenario just fine.







share|improve this answer












share|improve this answer



share|improve this answer










answered Aug 12 at 8:35









JohnbotJohnbot

2,3398 silver badges16 bronze badges




2,3398 silver badges16 bronze badges














  • $begingroup$
    Oooh. Nice one.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 9:55










  • $begingroup$
    I fixed the code with a ToList() . I also added your code @johnbot to the unit tests
    $endgroup$
    – Melbourne Developer
    Aug 12 at 10:00











  • $begingroup$
    No, adding ToList() at calling point won't make it thread-safe. Side question: while all those static methods are...fields?!
    $endgroup$
    – Adriano Repetti
    Aug 12 at 14:09










  • $begingroup$
    @Johnbot you found a glitch in the code and broke the thread safety. I believe you succeeded in the challenge. I was hoping to see if there were more fundamental issues that could break the thread safety, but you definitely achieved the aim.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 21:57
















  • $begingroup$
    Oooh. Nice one.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 9:55










  • $begingroup$
    I fixed the code with a ToList() . I also added your code @johnbot to the unit tests
    $endgroup$
    – Melbourne Developer
    Aug 12 at 10:00











  • $begingroup$
    No, adding ToList() at calling point won't make it thread-safe. Side question: while all those static methods are...fields?!
    $endgroup$
    – Adriano Repetti
    Aug 12 at 14:09










  • $begingroup$
    @Johnbot you found a glitch in the code and broke the thread safety. I believe you succeeded in the challenge. I was hoping to see if there were more fundamental issues that could break the thread safety, but you definitely achieved the aim.
    $endgroup$
    – Melbourne Developer
    Aug 12 at 21:57















$begingroup$
Oooh. Nice one.
$endgroup$
– Melbourne Developer
Aug 12 at 9:55




$begingroup$
Oooh. Nice one.
$endgroup$
– Melbourne Developer
Aug 12 at 9:55












$begingroup$
I fixed the code with a ToList() . I also added your code @johnbot to the unit tests
$endgroup$
– Melbourne Developer
Aug 12 at 10:00





$begingroup$
I fixed the code with a ToList() . I also added your code @johnbot to the unit tests
$endgroup$
– Melbourne Developer
Aug 12 at 10:00













$begingroup$
No, adding ToList() at calling point won't make it thread-safe. Side question: while all those static methods are...fields?!
$endgroup$
– Adriano Repetti
Aug 12 at 14:09




$begingroup$
No, adding ToList() at calling point won't make it thread-safe. Side question: while all those static methods are...fields?!
$endgroup$
– Adriano Repetti
Aug 12 at 14:09












$begingroup$
@Johnbot you found a glitch in the code and broke the thread safety. I believe you succeeded in the challenge. I was hoping to see if there were more fundamental issues that could break the thread safety, but you definitely achieved the aim.
$endgroup$
– Melbourne Developer
Aug 12 at 21:57




$begingroup$
@Johnbot you found a glitch in the code and broke the thread safety. I believe you succeeded in the challenge. I was hoping to see if there were more fundamental issues that could break the thread safety, but you definitely achieved the aim.
$endgroup$
– Melbourne Developer
Aug 12 at 21:57













24













$begingroup$

The reason why there is no async API for a dictionary is, that all operations on a dictionary are so fast, that there is no need for asynchronicity.



For concurrent scenarios there is the thread safe variant - the ConcurrentDictionary.



Adding an async API to these dictionaries has absolutely zero value. Rather it increases complexity and reduces performance.






share|improve this answer











$endgroup$










  • 2




    $begingroup$
    I'm starting to wonder if the above is true or not.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 5




    $begingroup$
    No developer is immune to getting lost in technical solutions that have no resistance at the end of the day. I think it is not even a question of experience / professionalism but rather a question of sharing ideas with colleagues / others. And even that may produce questionable solutions ;)
    $endgroup$
    – JanDotNet
    Aug 11 at 9:18






  • 1




    $begingroup$
    I think that what you have said, along with another answer are strong points that need to be taken in to consideration. I think to some extent, the question in my mind: "Why didn't the C# team create this?" has been answered to a large extent. But this wasn't the original challenge. The original challenge was to break the functionality of the class. The question of whether this class is useful or not is a separate question. I'm leaning toward thinking that it is not. However, IF it is not, I will explore that as a separate question.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:59






  • 1




    $begingroup$
    @MelbourneDeveloper: If you could batch up a bundle of work (like multiple key/value pairs to add), getting that all done while holding the lock once could be good for throughput vs. using atomic RMW operations for each one. (And bad for latency and maybe fairness if you aren't careful.) Or if you're getting false sharing with a concurrent hash table (threads on different cores modifying nearby entries), that could be worth trying to do something about. But inter-thread communication with callbacks is probably more expensive than inter-core latency for one cache miss.
    $endgroup$
    – Peter Cordes
    Aug 12 at 6:45






  • 1




    $begingroup$
    @MelbourneDeveloper - It is not efficient. The point of async is to wait on IO operations, because they are really, really slow (compared to the CPU anyway) and do not involve your thread. The idea is that during the IO operation your thread can do something else more worthwhile. Reading/writing to a dictionary however is pure memory shuffling, no IO whatsoever. And it all involves your thread. There's no moment when you could "wait" for something to happen in the background. Queueing some work to run in a background thread (Task.Run()) is purely artificial and unnecessary.
    $endgroup$
    – Vilx-
    Aug 12 at 16:14















24













$begingroup$

The reason why there is no async API for a dictionary is, that all operations on a dictionary are so fast, that there is no need for asynchronicity.



For concurrent scenarios there is the thread safe variant - the ConcurrentDictionary.



Adding an async API to these dictionaries has absolutely zero value. Rather it increases complexity and reduces performance.






share|improve this answer











$endgroup$










  • 2




    $begingroup$
    I'm starting to wonder if the above is true or not.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 5




    $begingroup$
    No developer is immune to getting lost in technical solutions that have no resistance at the end of the day. I think it is not even a question of experience / professionalism but rather a question of sharing ideas with colleagues / others. And even that may produce questionable solutions ;)
    $endgroup$
    – JanDotNet
    Aug 11 at 9:18






  • 1




    $begingroup$
    I think that what you have said, along with another answer are strong points that need to be taken in to consideration. I think to some extent, the question in my mind: "Why didn't the C# team create this?" has been answered to a large extent. But this wasn't the original challenge. The original challenge was to break the functionality of the class. The question of whether this class is useful or not is a separate question. I'm leaning toward thinking that it is not. However, IF it is not, I will explore that as a separate question.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:59






  • 1




    $begingroup$
    @MelbourneDeveloper: If you could batch up a bundle of work (like multiple key/value pairs to add), getting that all done while holding the lock once could be good for throughput vs. using atomic RMW operations for each one. (And bad for latency and maybe fairness if you aren't careful.) Or if you're getting false sharing with a concurrent hash table (threads on different cores modifying nearby entries), that could be worth trying to do something about. But inter-thread communication with callbacks is probably more expensive than inter-core latency for one cache miss.
    $endgroup$
    – Peter Cordes
    Aug 12 at 6:45






  • 1




    $begingroup$
    @MelbourneDeveloper - It is not efficient. The point of async is to wait on IO operations, because they are really, really slow (compared to the CPU anyway) and do not involve your thread. The idea is that during the IO operation your thread can do something else more worthwhile. Reading/writing to a dictionary however is pure memory shuffling, no IO whatsoever. And it all involves your thread. There's no moment when you could "wait" for something to happen in the background. Queueing some work to run in a background thread (Task.Run()) is purely artificial and unnecessary.
    $endgroup$
    – Vilx-
    Aug 12 at 16:14













24














24










24







$begingroup$

The reason why there is no async API for a dictionary is, that all operations on a dictionary are so fast, that there is no need for asynchronicity.



For concurrent scenarios there is the thread safe variant - the ConcurrentDictionary.



Adding an async API to these dictionaries has absolutely zero value. Rather it increases complexity and reduces performance.






share|improve this answer











$endgroup$



The reason why there is no async API for a dictionary is, that all operations on a dictionary are so fast, that there is no need for asynchronicity.



For concurrent scenarios there is the thread safe variant - the ConcurrentDictionary.



Adding an async API to these dictionaries has absolutely zero value. Rather it increases complexity and reduces performance.







share|improve this answer














share|improve this answer



share|improve this answer








edited Aug 12 at 10:30









piojo

1031 bronze badge




1031 bronze badge










answered Aug 11 at 7:43









JanDotNetJanDotNet

7,48814 silver badges40 bronze badges




7,48814 silver badges40 bronze badges










  • 2




    $begingroup$
    I'm starting to wonder if the above is true or not.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 5




    $begingroup$
    No developer is immune to getting lost in technical solutions that have no resistance at the end of the day. I think it is not even a question of experience / professionalism but rather a question of sharing ideas with colleagues / others. And even that may produce questionable solutions ;)
    $endgroup$
    – JanDotNet
    Aug 11 at 9:18






  • 1




    $begingroup$
    I think that what you have said, along with another answer are strong points that need to be taken in to consideration. I think to some extent, the question in my mind: "Why didn't the C# team create this?" has been answered to a large extent. But this wasn't the original challenge. The original challenge was to break the functionality of the class. The question of whether this class is useful or not is a separate question. I'm leaning toward thinking that it is not. However, IF it is not, I will explore that as a separate question.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:59






  • 1




    $begingroup$
    @MelbourneDeveloper: If you could batch up a bundle of work (like multiple key/value pairs to add), getting that all done while holding the lock once could be good for throughput vs. using atomic RMW operations for each one. (And bad for latency and maybe fairness if you aren't careful.) Or if you're getting false sharing with a concurrent hash table (threads on different cores modifying nearby entries), that could be worth trying to do something about. But inter-thread communication with callbacks is probably more expensive than inter-core latency for one cache miss.
    $endgroup$
    – Peter Cordes
    Aug 12 at 6:45






  • 1




    $begingroup$
    @MelbourneDeveloper - It is not efficient. The point of async is to wait on IO operations, because they are really, really slow (compared to the CPU anyway) and do not involve your thread. The idea is that during the IO operation your thread can do something else more worthwhile. Reading/writing to a dictionary however is pure memory shuffling, no IO whatsoever. And it all involves your thread. There's no moment when you could "wait" for something to happen in the background. Queueing some work to run in a background thread (Task.Run()) is purely artificial and unnecessary.
    $endgroup$
    – Vilx-
    Aug 12 at 16:14












  • 2




    $begingroup$
    I'm starting to wonder if the above is true or not.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 5




    $begingroup$
    No developer is immune to getting lost in technical solutions that have no resistance at the end of the day. I think it is not even a question of experience / professionalism but rather a question of sharing ideas with colleagues / others. And even that may produce questionable solutions ;)
    $endgroup$
    – JanDotNet
    Aug 11 at 9:18






  • 1




    $begingroup$
    I think that what you have said, along with another answer are strong points that need to be taken in to consideration. I think to some extent, the question in my mind: "Why didn't the C# team create this?" has been answered to a large extent. But this wasn't the original challenge. The original challenge was to break the functionality of the class. The question of whether this class is useful or not is a separate question. I'm leaning toward thinking that it is not. However, IF it is not, I will explore that as a separate question.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:59






  • 1




    $begingroup$
    @MelbourneDeveloper: If you could batch up a bundle of work (like multiple key/value pairs to add), getting that all done while holding the lock once could be good for throughput vs. using atomic RMW operations for each one. (And bad for latency and maybe fairness if you aren't careful.) Or if you're getting false sharing with a concurrent hash table (threads on different cores modifying nearby entries), that could be worth trying to do something about. But inter-thread communication with callbacks is probably more expensive than inter-core latency for one cache miss.
    $endgroup$
    – Peter Cordes
    Aug 12 at 6:45






  • 1




    $begingroup$
    @MelbourneDeveloper - It is not efficient. The point of async is to wait on IO operations, because they are really, really slow (compared to the CPU anyway) and do not involve your thread. The idea is that during the IO operation your thread can do something else more worthwhile. Reading/writing to a dictionary however is pure memory shuffling, no IO whatsoever. And it all involves your thread. There's no moment when you could "wait" for something to happen in the background. Queueing some work to run in a background thread (Task.Run()) is purely artificial and unnecessary.
    $endgroup$
    – Vilx-
    Aug 12 at 16:14







2




2




$begingroup$
I'm starting to wonder if the above is true or not.
$endgroup$
– Melbourne Developer
Aug 11 at 8:56




$begingroup$
I'm starting to wonder if the above is true or not.
$endgroup$
– Melbourne Developer
Aug 11 at 8:56




5




5




$begingroup$
No developer is immune to getting lost in technical solutions that have no resistance at the end of the day. I think it is not even a question of experience / professionalism but rather a question of sharing ideas with colleagues / others. And even that may produce questionable solutions ;)
$endgroup$
– JanDotNet
Aug 11 at 9:18




$begingroup$
No developer is immune to getting lost in technical solutions that have no resistance at the end of the day. I think it is not even a question of experience / professionalism but rather a question of sharing ideas with colleagues / others. And even that may produce questionable solutions ;)
$endgroup$
– JanDotNet
Aug 11 at 9:18




1




1




$begingroup$
I think that what you have said, along with another answer are strong points that need to be taken in to consideration. I think to some extent, the question in my mind: "Why didn't the C# team create this?" has been answered to a large extent. But this wasn't the original challenge. The original challenge was to break the functionality of the class. The question of whether this class is useful or not is a separate question. I'm leaning toward thinking that it is not. However, IF it is not, I will explore that as a separate question.
$endgroup$
– Melbourne Developer
Aug 11 at 22:59




$begingroup$
I think that what you have said, along with another answer are strong points that need to be taken in to consideration. I think to some extent, the question in my mind: "Why didn't the C# team create this?" has been answered to a large extent. But this wasn't the original challenge. The original challenge was to break the functionality of the class. The question of whether this class is useful or not is a separate question. I'm leaning toward thinking that it is not. However, IF it is not, I will explore that as a separate question.
$endgroup$
– Melbourne Developer
Aug 11 at 22:59




1




1




$begingroup$
@MelbourneDeveloper: If you could batch up a bundle of work (like multiple key/value pairs to add), getting that all done while holding the lock once could be good for throughput vs. using atomic RMW operations for each one. (And bad for latency and maybe fairness if you aren't careful.) Or if you're getting false sharing with a concurrent hash table (threads on different cores modifying nearby entries), that could be worth trying to do something about. But inter-thread communication with callbacks is probably more expensive than inter-core latency for one cache miss.
$endgroup$
– Peter Cordes
Aug 12 at 6:45




$begingroup$
@MelbourneDeveloper: If you could batch up a bundle of work (like multiple key/value pairs to add), getting that all done while holding the lock once could be good for throughput vs. using atomic RMW operations for each one. (And bad for latency and maybe fairness if you aren't careful.) Or if you're getting false sharing with a concurrent hash table (threads on different cores modifying nearby entries), that could be worth trying to do something about. But inter-thread communication with callbacks is probably more expensive than inter-core latency for one cache miss.
$endgroup$
– Peter Cordes
Aug 12 at 6:45




1




1




$begingroup$
@MelbourneDeveloper - It is not efficient. The point of async is to wait on IO operations, because they are really, really slow (compared to the CPU anyway) and do not involve your thread. The idea is that during the IO operation your thread can do something else more worthwhile. Reading/writing to a dictionary however is pure memory shuffling, no IO whatsoever. And it all involves your thread. There's no moment when you could "wait" for something to happen in the background. Queueing some work to run in a background thread (Task.Run()) is purely artificial and unnecessary.
$endgroup$
– Vilx-
Aug 12 at 16:14




$begingroup$
@MelbourneDeveloper - It is not efficient. The point of async is to wait on IO operations, because they are really, really slow (compared to the CPU anyway) and do not involve your thread. The idea is that during the IO operation your thread can do something else more worthwhile. Reading/writing to a dictionary however is pure memory shuffling, no IO whatsoever. And it all involves your thread. There's no moment when you could "wait" for something to happen in the background. Queueing some work to run in a background thread (Task.Run()) is purely artificial and unnecessary.
$endgroup$
– Vilx-
Aug 12 at 16:14











11













$begingroup$

It's pretty hard to break something that uses a global lock around everything. So this seems pretty thread-safe. But that doesn't answer the question of why you'd want to use this.



Asynchronous calls are useful for things that take a long time, particularly if you can delegate the "waiting" to some low-level event based solution (HTTP requests for example).



For anything else, they not only make for a very awkward use of the API, they also are rather slow and involve a lot of compiler generated machinery (which if you're using it for a long running task won't matter much, but for something as simple as adding to a dictionary is plain awful).



When optimising, it's always important to first figure out what you want to optimise (what scenarios are you particularly worried about? how many readers to writers? how much 'global' concurrency on the dictionary? how much races on existing keys?) and then to measure.



Just as a baseline here is the overhead your solution has when simply adding elements sequentially compared to a concurrent dictionary, the standard dictionary and the standard dictionary with a simple lock around it:



| Method | N | Mean | Error | StdDev | Ratio |
|------------------ |-------- |------------:|-----------:|-----------:|------:|
| AddSimple | 1000000 | 34.94 ms | 0.4827 ms | 0.4515 ms | 1.00 |
| | | | | | |
| AddSimpleWithLock | 1000000 | 52.14 ms | 0.7828 ms | 0.7323 ms | 1.00 |
| | | | | | |
| ConcurrentDic | 1000000 | 241.46 ms | 1.2975 ms | 1.2137 ms | 1.00 |
| | | | | | |
| AddAsync | 1000000 | 3,214.79 ms | 30.9326 ms | 28.9344 ms | 1.00 |


The following code was used to generate the results. If you haven't used BenchmarkDotNet before it's recommended to read up on it first to avoid getting incorrect results, but you can easily extend it for your other more interesting scenarios. In any case, the results are even worse than I imagined them to be (15 times slower than the concurrent dictionary is impressive even with all the boxing and async overhead - I guessed it'd be about 10 times at the start). (What shouldn't come as a surprise is how cheap uncontended locks are these days, everyone always running to ConcurrentDictionary and co might want to rethink that).



[ClrJob(baseline: true)]
public class DictionaryComparison

private Random _rand;

private Dictionary<int, int> _simpleDic;

private Dictionary<int, int> _simpleDicWithLock;

private readonly object _lock = new object();

private ConcurrentDictionary<int, int> _concurrentDic;

private AsyncDictionary<int, int> _asyncDictionary;

[Params( 1_000_000)]
public int N;

[IterationSetup]
public void IterationSetup()

_rand = new Random(0xdead);
_simpleDic = new Dictionary<int, int>();
_simpleDicWithLock = new Dictionary<int, int>();
_concurrentDic = new ConcurrentDictionary<int, int>();
_asyncDictionary = new AsyncDictionary<int, int>();


[Benchmark]
public void AddSimple()

for (int i = 0; i < N; i++)

_simpleDic[i] = i;



[Benchmark]
public void AddSimpleWithLock()

for (int i = 0; i < N; i++)

lock (_lock)

_simpleDicWithLock[i] = i;




[Benchmark]
public void ConcurrentDic()

for (int i = 0; i < N; i++)

_concurrentDic[i] = i;



[Benchmark]
public async Task AddAsync()

for (int i = 0; i < N; i++)

await _asyncDictionary.AddAsync(i, i);






public sealed class Program

private static async Task Main(string[] args)

var summary = BenchmarkRunner.Run<DictionaryComparison>();




PS: The work is so little even when adding a million elements, that the results are a bit dubious for the two AddSimple variants.






share|improve this answer











$endgroup$














  • $begingroup$
    This is very valuable information. I thank you for your time and rigour. I wasn't naive enough to think that this class would outperform ConcurrentDictionary. You've also introduced me to a useful technology BenchmarkDotNet and have made me question some of my original assumptions.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:50










  • $begingroup$
    What about performance of the async wrapper of ConcurrentDic I presented?
    $endgroup$
    – dfhwze
    Aug 12 at 3:56










  • $begingroup$
    @dfhwze The takeaway here should be to not introduce asynchronous wrapper APIs on top of synchronous APIs. Asynchronous code is mostly useful if you don't have to block a thread during the waiting process (http calls or timers are two classic examples). There simply is no benefit to be gained from wrapping here.
    $endgroup$
    – Voo
    Aug 12 at 4:32










  • $begingroup$
    I thought the whole point of this question was to have an async signature for dictionary operations :-(
    $endgroup$
    – dfhwze
    Aug 12 at 4:35






  • 4




    $begingroup$
    @dfhwze "They were so preoccupied with whether or not they could, they didn’t stop to think if they should." ;) It looks like a classical XY problem.
    $endgroup$
    – Voo
    Aug 12 at 6:05















11













$begingroup$

It's pretty hard to break something that uses a global lock around everything. So this seems pretty thread-safe. But that doesn't answer the question of why you'd want to use this.



Asynchronous calls are useful for things that take a long time, particularly if you can delegate the "waiting" to some low-level event based solution (HTTP requests for example).



For anything else, they not only make for a very awkward use of the API, they also are rather slow and involve a lot of compiler generated machinery (which if you're using it for a long running task won't matter much, but for something as simple as adding to a dictionary is plain awful).



When optimising, it's always important to first figure out what you want to optimise (what scenarios are you particularly worried about? how many readers to writers? how much 'global' concurrency on the dictionary? how much races on existing keys?) and then to measure.



Just as a baseline here is the overhead your solution has when simply adding elements sequentially compared to a concurrent dictionary, the standard dictionary and the standard dictionary with a simple lock around it:



| Method | N | Mean | Error | StdDev | Ratio |
|------------------ |-------- |------------:|-----------:|-----------:|------:|
| AddSimple | 1000000 | 34.94 ms | 0.4827 ms | 0.4515 ms | 1.00 |
| | | | | | |
| AddSimpleWithLock | 1000000 | 52.14 ms | 0.7828 ms | 0.7323 ms | 1.00 |
| | | | | | |
| ConcurrentDic | 1000000 | 241.46 ms | 1.2975 ms | 1.2137 ms | 1.00 |
| | | | | | |
| AddAsync | 1000000 | 3,214.79 ms | 30.9326 ms | 28.9344 ms | 1.00 |


The following code was used to generate the results. If you haven't used BenchmarkDotNet before it's recommended to read up on it first to avoid getting incorrect results, but you can easily extend it for your other more interesting scenarios. In any case, the results are even worse than I imagined them to be (15 times slower than the concurrent dictionary is impressive even with all the boxing and async overhead - I guessed it'd be about 10 times at the start). (What shouldn't come as a surprise is how cheap uncontended locks are these days, everyone always running to ConcurrentDictionary and co might want to rethink that).



[ClrJob(baseline: true)]
public class DictionaryComparison

private Random _rand;

private Dictionary<int, int> _simpleDic;

private Dictionary<int, int> _simpleDicWithLock;

private readonly object _lock = new object();

private ConcurrentDictionary<int, int> _concurrentDic;

private AsyncDictionary<int, int> _asyncDictionary;

[Params( 1_000_000)]
public int N;

[IterationSetup]
public void IterationSetup()

_rand = new Random(0xdead);
_simpleDic = new Dictionary<int, int>();
_simpleDicWithLock = new Dictionary<int, int>();
_concurrentDic = new ConcurrentDictionary<int, int>();
_asyncDictionary = new AsyncDictionary<int, int>();


[Benchmark]
public void AddSimple()

for (int i = 0; i < N; i++)

_simpleDic[i] = i;



[Benchmark]
public void AddSimpleWithLock()

for (int i = 0; i < N; i++)

lock (_lock)

_simpleDicWithLock[i] = i;




[Benchmark]
public void ConcurrentDic()

for (int i = 0; i < N; i++)

_concurrentDic[i] = i;



[Benchmark]
public async Task AddAsync()

for (int i = 0; i < N; i++)

await _asyncDictionary.AddAsync(i, i);






public sealed class Program

private static async Task Main(string[] args)

var summary = BenchmarkRunner.Run<DictionaryComparison>();




PS: The work is so little even when adding a million elements, that the results are a bit dubious for the two AddSimple variants.






share|improve this answer











$endgroup$














  • $begingroup$
    This is very valuable information. I thank you for your time and rigour. I wasn't naive enough to think that this class would outperform ConcurrentDictionary. You've also introduced me to a useful technology BenchmarkDotNet and have made me question some of my original assumptions.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:50










  • $begingroup$
    What about performance of the async wrapper of ConcurrentDic I presented?
    $endgroup$
    – dfhwze
    Aug 12 at 3:56










  • $begingroup$
    @dfhwze The takeaway here should be to not introduce asynchronous wrapper APIs on top of synchronous APIs. Asynchronous code is mostly useful if you don't have to block a thread during the waiting process (http calls or timers are two classic examples). There simply is no benefit to be gained from wrapping here.
    $endgroup$
    – Voo
    Aug 12 at 4:32










  • $begingroup$
    I thought the whole point of this question was to have an async signature for dictionary operations :-(
    $endgroup$
    – dfhwze
    Aug 12 at 4:35






  • 4




    $begingroup$
    @dfhwze "They were so preoccupied with whether or not they could, they didn’t stop to think if they should." ;) It looks like a classical XY problem.
    $endgroup$
    – Voo
    Aug 12 at 6:05













11














11










11







$begingroup$

It's pretty hard to break something that uses a global lock around everything. So this seems pretty thread-safe. But that doesn't answer the question of why you'd want to use this.



Asynchronous calls are useful for things that take a long time, particularly if you can delegate the "waiting" to some low-level event based solution (HTTP requests for example).



For anything else, they not only make for a very awkward use of the API, they also are rather slow and involve a lot of compiler generated machinery (which if you're using it for a long running task won't matter much, but for something as simple as adding to a dictionary is plain awful).



When optimising, it's always important to first figure out what you want to optimise (what scenarios are you particularly worried about? how many readers to writers? how much 'global' concurrency on the dictionary? how much races on existing keys?) and then to measure.



Just as a baseline here is the overhead your solution has when simply adding elements sequentially compared to a concurrent dictionary, the standard dictionary and the standard dictionary with a simple lock around it:



| Method | N | Mean | Error | StdDev | Ratio |
|------------------ |-------- |------------:|-----------:|-----------:|------:|
| AddSimple | 1000000 | 34.94 ms | 0.4827 ms | 0.4515 ms | 1.00 |
| | | | | | |
| AddSimpleWithLock | 1000000 | 52.14 ms | 0.7828 ms | 0.7323 ms | 1.00 |
| | | | | | |
| ConcurrentDic | 1000000 | 241.46 ms | 1.2975 ms | 1.2137 ms | 1.00 |
| | | | | | |
| AddAsync | 1000000 | 3,214.79 ms | 30.9326 ms | 28.9344 ms | 1.00 |


The following code was used to generate the results. If you haven't used BenchmarkDotNet before it's recommended to read up on it first to avoid getting incorrect results, but you can easily extend it for your other more interesting scenarios. In any case, the results are even worse than I imagined them to be (15 times slower than the concurrent dictionary is impressive even with all the boxing and async overhead - I guessed it'd be about 10 times at the start). (What shouldn't come as a surprise is how cheap uncontended locks are these days, everyone always running to ConcurrentDictionary and co might want to rethink that).



[ClrJob(baseline: true)]
public class DictionaryComparison

private Random _rand;

private Dictionary<int, int> _simpleDic;

private Dictionary<int, int> _simpleDicWithLock;

private readonly object _lock = new object();

private ConcurrentDictionary<int, int> _concurrentDic;

private AsyncDictionary<int, int> _asyncDictionary;

[Params( 1_000_000)]
public int N;

[IterationSetup]
public void IterationSetup()

_rand = new Random(0xdead);
_simpleDic = new Dictionary<int, int>();
_simpleDicWithLock = new Dictionary<int, int>();
_concurrentDic = new ConcurrentDictionary<int, int>();
_asyncDictionary = new AsyncDictionary<int, int>();


[Benchmark]
public void AddSimple()

for (int i = 0; i < N; i++)

_simpleDic[i] = i;



[Benchmark]
public void AddSimpleWithLock()

for (int i = 0; i < N; i++)

lock (_lock)

_simpleDicWithLock[i] = i;




[Benchmark]
public void ConcurrentDic()

for (int i = 0; i < N; i++)

_concurrentDic[i] = i;



[Benchmark]
public async Task AddAsync()

for (int i = 0; i < N; i++)

await _asyncDictionary.AddAsync(i, i);






public sealed class Program

private static async Task Main(string[] args)

var summary = BenchmarkRunner.Run<DictionaryComparison>();




PS: The work is so little even when adding a million elements, that the results are a bit dubious for the two AddSimple variants.






share|improve this answer











$endgroup$



It's pretty hard to break something that uses a global lock around everything. So this seems pretty thread-safe. But that doesn't answer the question of why you'd want to use this.



Asynchronous calls are useful for things that take a long time, particularly if you can delegate the "waiting" to some low-level event based solution (HTTP requests for example).



For anything else, they not only make for a very awkward use of the API, they also are rather slow and involve a lot of compiler generated machinery (which if you're using it for a long running task won't matter much, but for something as simple as adding to a dictionary is plain awful).



When optimising, it's always important to first figure out what you want to optimise (what scenarios are you particularly worried about? how many readers to writers? how much 'global' concurrency on the dictionary? how much races on existing keys?) and then to measure.



Just as a baseline here is the overhead your solution has when simply adding elements sequentially compared to a concurrent dictionary, the standard dictionary and the standard dictionary with a simple lock around it:



| Method | N | Mean | Error | StdDev | Ratio |
|------------------ |-------- |------------:|-----------:|-----------:|------:|
| AddSimple | 1000000 | 34.94 ms | 0.4827 ms | 0.4515 ms | 1.00 |
| | | | | | |
| AddSimpleWithLock | 1000000 | 52.14 ms | 0.7828 ms | 0.7323 ms | 1.00 |
| | | | | | |
| ConcurrentDic | 1000000 | 241.46 ms | 1.2975 ms | 1.2137 ms | 1.00 |
| | | | | | |
| AddAsync | 1000000 | 3,214.79 ms | 30.9326 ms | 28.9344 ms | 1.00 |


The following code was used to generate the results. If you haven't used BenchmarkDotNet before it's recommended to read up on it first to avoid getting incorrect results, but you can easily extend it for your other more interesting scenarios. In any case, the results are even worse than I imagined them to be (15 times slower than the concurrent dictionary is impressive even with all the boxing and async overhead - I guessed it'd be about 10 times at the start). (What shouldn't come as a surprise is how cheap uncontended locks are these days, everyone always running to ConcurrentDictionary and co might want to rethink that).



[ClrJob(baseline: true)]
public class DictionaryComparison

private Random _rand;

private Dictionary<int, int> _simpleDic;

private Dictionary<int, int> _simpleDicWithLock;

private readonly object _lock = new object();

private ConcurrentDictionary<int, int> _concurrentDic;

private AsyncDictionary<int, int> _asyncDictionary;

[Params( 1_000_000)]
public int N;

[IterationSetup]
public void IterationSetup()

_rand = new Random(0xdead);
_simpleDic = new Dictionary<int, int>();
_simpleDicWithLock = new Dictionary<int, int>();
_concurrentDic = new ConcurrentDictionary<int, int>();
_asyncDictionary = new AsyncDictionary<int, int>();


[Benchmark]
public void AddSimple()

for (int i = 0; i < N; i++)

_simpleDic[i] = i;



[Benchmark]
public void AddSimpleWithLock()

for (int i = 0; i < N; i++)

lock (_lock)

_simpleDicWithLock[i] = i;




[Benchmark]
public void ConcurrentDic()

for (int i = 0; i < N; i++)

_concurrentDic[i] = i;



[Benchmark]
public async Task AddAsync()

for (int i = 0; i < N; i++)

await _asyncDictionary.AddAsync(i, i);






public sealed class Program

private static async Task Main(string[] args)

var summary = BenchmarkRunner.Run<DictionaryComparison>();




PS: The work is so little even when adding a million elements, that the results are a bit dubious for the two AddSimple variants.







share|improve this answer














share|improve this answer



share|improve this answer








edited Aug 11 at 21:43

























answered Aug 11 at 21:35









VooVoo

5071 gold badge3 silver badges11 bronze badges




5071 gold badge3 silver badges11 bronze badges














  • $begingroup$
    This is very valuable information. I thank you for your time and rigour. I wasn't naive enough to think that this class would outperform ConcurrentDictionary. You've also introduced me to a useful technology BenchmarkDotNet and have made me question some of my original assumptions.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:50










  • $begingroup$
    What about performance of the async wrapper of ConcurrentDic I presented?
    $endgroup$
    – dfhwze
    Aug 12 at 3:56










  • $begingroup$
    @dfhwze The takeaway here should be to not introduce asynchronous wrapper APIs on top of synchronous APIs. Asynchronous code is mostly useful if you don't have to block a thread during the waiting process (http calls or timers are two classic examples). There simply is no benefit to be gained from wrapping here.
    $endgroup$
    – Voo
    Aug 12 at 4:32










  • $begingroup$
    I thought the whole point of this question was to have an async signature for dictionary operations :-(
    $endgroup$
    – dfhwze
    Aug 12 at 4:35






  • 4




    $begingroup$
    @dfhwze "They were so preoccupied with whether or not they could, they didn’t stop to think if they should." ;) It looks like a classical XY problem.
    $endgroup$
    – Voo
    Aug 12 at 6:05
















  • $begingroup$
    This is very valuable information. I thank you for your time and rigour. I wasn't naive enough to think that this class would outperform ConcurrentDictionary. You've also introduced me to a useful technology BenchmarkDotNet and have made me question some of my original assumptions.
    $endgroup$
    – Melbourne Developer
    Aug 11 at 22:50










  • $begingroup$
    What about performance of the async wrapper of ConcurrentDic I presented?
    $endgroup$
    – dfhwze
    Aug 12 at 3:56










  • $begingroup$
    @dfhwze The takeaway here should be to not introduce asynchronous wrapper APIs on top of synchronous APIs. Asynchronous code is mostly useful if you don't have to block a thread during the waiting process (http calls or timers are two classic examples). There simply is no benefit to be gained from wrapping here.
    $endgroup$
    – Voo
    Aug 12 at 4:32










  • $begingroup$
    I thought the whole point of this question was to have an async signature for dictionary operations :-(
    $endgroup$
    – dfhwze
    Aug 12 at 4:35






  • 4




    $begingroup$
    @dfhwze "They were so preoccupied with whether or not they could, they didn’t stop to think if they should." ;) It looks like a classical XY problem.
    $endgroup$
    – Voo
    Aug 12 at 6:05















$begingroup$
This is very valuable information. I thank you for your time and rigour. I wasn't naive enough to think that this class would outperform ConcurrentDictionary. You've also introduced me to a useful technology BenchmarkDotNet and have made me question some of my original assumptions.
$endgroup$
– Melbourne Developer
Aug 11 at 22:50




$begingroup$
This is very valuable information. I thank you for your time and rigour. I wasn't naive enough to think that this class would outperform ConcurrentDictionary. You've also introduced me to a useful technology BenchmarkDotNet and have made me question some of my original assumptions.
$endgroup$
– Melbourne Developer
Aug 11 at 22:50












$begingroup$
What about performance of the async wrapper of ConcurrentDic I presented?
$endgroup$
– dfhwze
Aug 12 at 3:56




$begingroup$
What about performance of the async wrapper of ConcurrentDic I presented?
$endgroup$
– dfhwze
Aug 12 at 3:56












$begingroup$
@dfhwze The takeaway here should be to not introduce asynchronous wrapper APIs on top of synchronous APIs. Asynchronous code is mostly useful if you don't have to block a thread during the waiting process (http calls or timers are two classic examples). There simply is no benefit to be gained from wrapping here.
$endgroup$
– Voo
Aug 12 at 4:32




$begingroup$
@dfhwze The takeaway here should be to not introduce asynchronous wrapper APIs on top of synchronous APIs. Asynchronous code is mostly useful if you don't have to block a thread during the waiting process (http calls or timers are two classic examples). There simply is no benefit to be gained from wrapping here.
$endgroup$
– Voo
Aug 12 at 4:32












$begingroup$
I thought the whole point of this question was to have an async signature for dictionary operations :-(
$endgroup$
– dfhwze
Aug 12 at 4:35




$begingroup$
I thought the whole point of this question was to have an async signature for dictionary operations :-(
$endgroup$
– dfhwze
Aug 12 at 4:35




4




4




$begingroup$
@dfhwze "They were so preoccupied with whether or not they could, they didn’t stop to think if they should." ;) It looks like a classical XY problem.
$endgroup$
– Voo
Aug 12 at 6:05




$begingroup$
@dfhwze "They were so preoccupied with whether or not they could, they didn’t stop to think if they should." ;) It looks like a classical XY problem.
$endgroup$
– Voo
Aug 12 at 6:05











11













$begingroup$

Threading Design



Your implementation has a very intrusive lock for all read and write operations, using the SemaphoreSlim with max concurrency 1.




try

await _semaphoreSlim.WaitAsync()// <- both read/write operations acquire single mutex

return await Task.Run(async () =>

return await func(_dictionary, keyValuePair);
);

finally

_semaphoreSlim.Release();




CallSynchronizedAsync is hence thread-safe to the extend you introduce possible fairness issues. In .NET, multiple threads awaiting a lock to get signaled are not notified in a fair way, meaning thread B may get the lock before A even if A asked to acquire the lock before B.



ConcurrentDictionary mitigates the risk on unfair thread signaling by using the following behavior:



  • Read operations are volatile/atomic. No locks are used (Source: TryGetValue).

  • Write operations are optimized for fast access and minimum lock timespans (Source: TryAddInternal).

Perhaps you should wrap a ConcurrentDictionary with async/await Task.Run or Task.FromResult functionality to take advantage of the built-in locking mechanism.




API Design



After discussing a bit with you in the comments, it became apparent you would like normal dictionary method signatures. This should not be a problem. Let's say you have a private field of type concurrent dictionary _dictionary. One option is to call the code synchronously and return Task.CompletedTask to make it fit the async/await pattern.



public async Task AddAsync(TKey key, TValue value)

_dictionary.TryAdd(key, value);
return Task.CompletedTask;



You could decide to also provide async/await-aware methods with concurrent dictionary signatures:



public async Task<bool> TryAddAsync(TKey key, TValue value)

return Task.FromResult(_dictionary.TryAdd(key, value));






share|improve this answer











$endgroup$










  • 1




    $begingroup$
    This post explains thread fairness: stackoverflow.com/questions/4228864/….
    $endgroup$
    – dfhwze
    Aug 11 at 6:56







  • 2




    $begingroup$
    sorry, it looks like I was wrong. ConcurrentDictionary does implement IDictionary so I'm rethinking some of my assumptions. Have a look at the diff in this branch. The unit test still passes BTW: github.com/MelbourneDeveloper/AsyncDictionary/compare/…
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 2




    $begingroup$
    There's literally (yes I did look up the meaning I'm a dictionary once ;)) no reason to ever wrap a short-lived synchronous API call such as adding to a (concurrent) dictionary in Task.Run. That's simply bad design. On a slightly related note: Most people use ConcurrentDictionary in circumstances where a simple dictionary with lock would be faster - just goes to show how complicated performance matters can be.
    $endgroup$
    – Voo
    Aug 11 at 21:55






  • 1




    $begingroup$
    @Voo my example wraps in Task.FromResult, not Task.Run. Would that be good practice?
    $endgroup$
    – dfhwze
    Aug 12 at 4:21






  • 1




    $begingroup$
    @dfhwze Task.FromResult is there to allow synchronous implementations to implement asynchronous interfaces easily and with minimal overhead. Given that you control both implementation and interface there's no need for it either.
    $endgroup$
    – Voo
    Aug 12 at 6:03















11













$begingroup$

Threading Design



Your implementation has a very intrusive lock for all read and write operations, using the SemaphoreSlim with max concurrency 1.




try

await _semaphoreSlim.WaitAsync()// <- both read/write operations acquire single mutex

return await Task.Run(async () =>

return await func(_dictionary, keyValuePair);
);

finally

_semaphoreSlim.Release();




CallSynchronizedAsync is hence thread-safe to the extend you introduce possible fairness issues. In .NET, multiple threads awaiting a lock to get signaled are not notified in a fair way, meaning thread B may get the lock before A even if A asked to acquire the lock before B.



ConcurrentDictionary mitigates the risk on unfair thread signaling by using the following behavior:



  • Read operations are volatile/atomic. No locks are used (Source: TryGetValue).

  • Write operations are optimized for fast access and minimum lock timespans (Source: TryAddInternal).

Perhaps you should wrap a ConcurrentDictionary with async/await Task.Run or Task.FromResult functionality to take advantage of the built-in locking mechanism.




API Design



After discussing a bit with you in the comments, it became apparent you would like normal dictionary method signatures. This should not be a problem. Let's say you have a private field of type concurrent dictionary _dictionary. One option is to call the code synchronously and return Task.CompletedTask to make it fit the async/await pattern.



public async Task AddAsync(TKey key, TValue value)

_dictionary.TryAdd(key, value);
return Task.CompletedTask;



You could decide to also provide async/await-aware methods with concurrent dictionary signatures:



public async Task<bool> TryAddAsync(TKey key, TValue value)

return Task.FromResult(_dictionary.TryAdd(key, value));






share|improve this answer











$endgroup$










  • 1




    $begingroup$
    This post explains thread fairness: stackoverflow.com/questions/4228864/….
    $endgroup$
    – dfhwze
    Aug 11 at 6:56







  • 2




    $begingroup$
    sorry, it looks like I was wrong. ConcurrentDictionary does implement IDictionary so I'm rethinking some of my assumptions. Have a look at the diff in this branch. The unit test still passes BTW: github.com/MelbourneDeveloper/AsyncDictionary/compare/…
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 2




    $begingroup$
    There's literally (yes I did look up the meaning I'm a dictionary once ;)) no reason to ever wrap a short-lived synchronous API call such as adding to a (concurrent) dictionary in Task.Run. That's simply bad design. On a slightly related note: Most people use ConcurrentDictionary in circumstances where a simple dictionary with lock would be faster - just goes to show how complicated performance matters can be.
    $endgroup$
    – Voo
    Aug 11 at 21:55






  • 1




    $begingroup$
    @Voo my example wraps in Task.FromResult, not Task.Run. Would that be good practice?
    $endgroup$
    – dfhwze
    Aug 12 at 4:21






  • 1




    $begingroup$
    @dfhwze Task.FromResult is there to allow synchronous implementations to implement asynchronous interfaces easily and with minimal overhead. Given that you control both implementation and interface there's no need for it either.
    $endgroup$
    – Voo
    Aug 12 at 6:03













11














11










11







$begingroup$

Threading Design



Your implementation has a very intrusive lock for all read and write operations, using the SemaphoreSlim with max concurrency 1.




try

await _semaphoreSlim.WaitAsync()// <- both read/write operations acquire single mutex

return await Task.Run(async () =>

return await func(_dictionary, keyValuePair);
);

finally

_semaphoreSlim.Release();




CallSynchronizedAsync is hence thread-safe to the extend you introduce possible fairness issues. In .NET, multiple threads awaiting a lock to get signaled are not notified in a fair way, meaning thread B may get the lock before A even if A asked to acquire the lock before B.



ConcurrentDictionary mitigates the risk on unfair thread signaling by using the following behavior:



  • Read operations are volatile/atomic. No locks are used (Source: TryGetValue).

  • Write operations are optimized for fast access and minimum lock timespans (Source: TryAddInternal).

Perhaps you should wrap a ConcurrentDictionary with async/await Task.Run or Task.FromResult functionality to take advantage of the built-in locking mechanism.




API Design



After discussing a bit with you in the comments, it became apparent you would like normal dictionary method signatures. This should not be a problem. Let's say you have a private field of type concurrent dictionary _dictionary. One option is to call the code synchronously and return Task.CompletedTask to make it fit the async/await pattern.



public async Task AddAsync(TKey key, TValue value)

_dictionary.TryAdd(key, value);
return Task.CompletedTask;



You could decide to also provide async/await-aware methods with concurrent dictionary signatures:



public async Task<bool> TryAddAsync(TKey key, TValue value)

return Task.FromResult(_dictionary.TryAdd(key, value));






share|improve this answer











$endgroup$



Threading Design



Your implementation has a very intrusive lock for all read and write operations, using the SemaphoreSlim with max concurrency 1.




try

await _semaphoreSlim.WaitAsync()// <- both read/write operations acquire single mutex

return await Task.Run(async () =>

return await func(_dictionary, keyValuePair);
);

finally

_semaphoreSlim.Release();




CallSynchronizedAsync is hence thread-safe to the extend you introduce possible fairness issues. In .NET, multiple threads awaiting a lock to get signaled are not notified in a fair way, meaning thread B may get the lock before A even if A asked to acquire the lock before B.



ConcurrentDictionary mitigates the risk on unfair thread signaling by using the following behavior:



  • Read operations are volatile/atomic. No locks are used (Source: TryGetValue).

  • Write operations are optimized for fast access and minimum lock timespans (Source: TryAddInternal).

Perhaps you should wrap a ConcurrentDictionary with async/await Task.Run or Task.FromResult functionality to take advantage of the built-in locking mechanism.




API Design



After discussing a bit with you in the comments, it became apparent you would like normal dictionary method signatures. This should not be a problem. Let's say you have a private field of type concurrent dictionary _dictionary. One option is to call the code synchronously and return Task.CompletedTask to make it fit the async/await pattern.



public async Task AddAsync(TKey key, TValue value)

_dictionary.TryAdd(key, value);
return Task.CompletedTask;



You could decide to also provide async/await-aware methods with concurrent dictionary signatures:



public async Task<bool> TryAddAsync(TKey key, TValue value)

return Task.FromResult(_dictionary.TryAdd(key, value));







share|improve this answer














share|improve this answer



share|improve this answer








edited Aug 12 at 4:23

























answered Aug 11 at 6:49









dfhwzedfhwze

9,1251 gold badge19 silver badges54 bronze badges




9,1251 gold badge19 silver badges54 bronze badges










  • 1




    $begingroup$
    This post explains thread fairness: stackoverflow.com/questions/4228864/….
    $endgroup$
    – dfhwze
    Aug 11 at 6:56







  • 2




    $begingroup$
    sorry, it looks like I was wrong. ConcurrentDictionary does implement IDictionary so I'm rethinking some of my assumptions. Have a look at the diff in this branch. The unit test still passes BTW: github.com/MelbourneDeveloper/AsyncDictionary/compare/…
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 2




    $begingroup$
    There's literally (yes I did look up the meaning I'm a dictionary once ;)) no reason to ever wrap a short-lived synchronous API call such as adding to a (concurrent) dictionary in Task.Run. That's simply bad design. On a slightly related note: Most people use ConcurrentDictionary in circumstances where a simple dictionary with lock would be faster - just goes to show how complicated performance matters can be.
    $endgroup$
    – Voo
    Aug 11 at 21:55






  • 1




    $begingroup$
    @Voo my example wraps in Task.FromResult, not Task.Run. Would that be good practice?
    $endgroup$
    – dfhwze
    Aug 12 at 4:21






  • 1




    $begingroup$
    @dfhwze Task.FromResult is there to allow synchronous implementations to implement asynchronous interfaces easily and with minimal overhead. Given that you control both implementation and interface there's no need for it either.
    $endgroup$
    – Voo
    Aug 12 at 6:03












  • 1




    $begingroup$
    This post explains thread fairness: stackoverflow.com/questions/4228864/….
    $endgroup$
    – dfhwze
    Aug 11 at 6:56







  • 2




    $begingroup$
    sorry, it looks like I was wrong. ConcurrentDictionary does implement IDictionary so I'm rethinking some of my assumptions. Have a look at the diff in this branch. The unit test still passes BTW: github.com/MelbourneDeveloper/AsyncDictionary/compare/…
    $endgroup$
    – Melbourne Developer
    Aug 11 at 8:56






  • 2




    $begingroup$
    There's literally (yes I did look up the meaning I'm a dictionary once ;)) no reason to ever wrap a short-lived synchronous API call such as adding to a (concurrent) dictionary in Task.Run. That's simply bad design. On a slightly related note: Most people use ConcurrentDictionary in circumstances where a simple dictionary with lock would be faster - just goes to show how complicated performance matters can be.
    $endgroup$
    – Voo
    Aug 11 at 21:55






  • 1




    $begingroup$
    @Voo my example wraps in Task.FromResult, not Task.Run. Would that be good practice?
    $endgroup$
    – dfhwze
    Aug 12 at 4:21






  • 1




    $begingroup$
    @dfhwze Task.FromResult is there to allow synchronous implementations to implement asynchronous interfaces easily and with minimal overhead. Given that you control both implementation and interface there's no need for it either.
    $endgroup$
    – Voo
    Aug 12 at 6:03







1




1




$begingroup$
This post explains thread fairness: stackoverflow.com/questions/4228864/….
$endgroup$
– dfhwze
Aug 11 at 6:56





$begingroup$
This post explains thread fairness: stackoverflow.com/questions/4228864/….
$endgroup$
– dfhwze
Aug 11 at 6:56





2




2




$begingroup$
sorry, it looks like I was wrong. ConcurrentDictionary does implement IDictionary so I'm rethinking some of my assumptions. Have a look at the diff in this branch. The unit test still passes BTW: github.com/MelbourneDeveloper/AsyncDictionary/compare/…
$endgroup$
– Melbourne Developer
Aug 11 at 8:56




$begingroup$
sorry, it looks like I was wrong. ConcurrentDictionary does implement IDictionary so I'm rethinking some of my assumptions. Have a look at the diff in this branch. The unit test still passes BTW: github.com/MelbourneDeveloper/AsyncDictionary/compare/…
$endgroup$
– Melbourne Developer
Aug 11 at 8:56




2




2




$begingroup$
There's literally (yes I did look up the meaning I'm a dictionary once ;)) no reason to ever wrap a short-lived synchronous API call such as adding to a (concurrent) dictionary in Task.Run. That's simply bad design. On a slightly related note: Most people use ConcurrentDictionary in circumstances where a simple dictionary with lock would be faster - just goes to show how complicated performance matters can be.
$endgroup$
– Voo
Aug 11 at 21:55




$begingroup$
There's literally (yes I did look up the meaning I'm a dictionary once ;)) no reason to ever wrap a short-lived synchronous API call such as adding to a (concurrent) dictionary in Task.Run. That's simply bad design. On a slightly related note: Most people use ConcurrentDictionary in circumstances where a simple dictionary with lock would be faster - just goes to show how complicated performance matters can be.
$endgroup$
– Voo
Aug 11 at 21:55




1




1




$begingroup$
@Voo my example wraps in Task.FromResult, not Task.Run. Would that be good practice?
$endgroup$
– dfhwze
Aug 12 at 4:21




$begingroup$
@Voo my example wraps in Task.FromResult, not Task.Run. Would that be good practice?
$endgroup$
– dfhwze
Aug 12 at 4:21




1




1




$begingroup$
@dfhwze Task.FromResult is there to allow synchronous implementations to implement asynchronous interfaces easily and with minimal overhead. Given that you control both implementation and interface there's no need for it either.
$endgroup$
– Voo
Aug 12 at 6:03




$begingroup$
@dfhwze Task.FromResult is there to allow synchronous implementations to implement asynchronous interfaces easily and with minimal overhead. Given that you control both implementation and interface there's no need for it either.
$endgroup$
– Voo
Aug 12 at 6:03

















draft saved

draft discarded
















































Thanks for contributing an answer to Code Review Stack Exchange!


  • Please be sure to answer the question. Provide details and share your research!

But avoid


  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.

Use MathJax to format equations. MathJax reference.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f225911%2fasyncdictionary-can-you-break-thread-safety%23new-answer', 'question_page');

);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

Get product attribute by attribute group code in magento 2get product attribute by product attribute group in magento 2Magento 2 Log Bundle Product Data in List Page?How to get all product attribute of a attribute group of Default attribute set?Magento 2.1 Create a filter in the product grid by new attributeMagento 2 : Get Product Attribute values By GroupMagento 2 How to get all existing values for one attributeMagento 2 get custom attribute of a single product inside a pluginMagento 2.3 How to get all the Multi Source Inventory (MSI) locations collection in custom module?Magento2: how to develop rest API to get new productsGet product attribute by attribute group code ( [attribute_group_code] ) in magento 2

Category:9 (number) SubcategoriesMedia in category "9 (number)"Navigation menuUpload mediaGND ID: 4485639-8Library of Congress authority ID: sh85091979ReasonatorScholiaStatistics

Magento 2.3: How do i solve this, Not registered handle, on custom form?How can i rewrite TierPrice Block in Magento2magento 2 captcha not rendering if I override layout xmlmain.CRITICAL: Plugin class doesn't existMagento 2 : Problem while adding custom button order view page?Magento 2.2.5: Overriding Admin Controller sales/orderMagento 2.2.5: Add, Update and Delete existing products Custom OptionsMagento 2.3 : File Upload issue in UI Component FormMagento2 Not registered handleHow to configured Form Builder Js in my custom magento 2.3.0 module?Magento 2.3. How to create image upload field in an admin form