This is a quick post showing you the behaviour of ConcurrentDictionary.GetOrAdd, which might not be what you expect. I bet that when you first read it, you think:
If the key exists, the corresponding value is returned; if not the
Func
is executed "Once", and the resulting value is associated with the key.
Are you sure about this? Lets have a look. The code below kicks of two tasks that will access GetOrAdd
for the same key. I use some threading synchronization techniques to get concurrent execution of the Func
. Like: SemaphoreSlim, SpinWait, Interlocked.Increment.
private static readonly StringBuilder Log = new StringBuilder();
private static readonly SemaphoreSlim Sync = new SemaphoreSlim(1, 1);
private static int _tasksStartedCount = 0;
static void Main(string[] args)
{
var dictionary = new ConcurrentDictionary<int, string>();
var key = 42;
//Semaphore is used to control the flow of the tasks.
Sync.Wait();
var task1 = Task.Factory.StartNew(() =>
{
Thread.CurrentThread.Name = "Task1's thread";
dictionary.GetOrAdd(key, CreateItem);
});
Log.AppendLine("Started Task1");
var task2 = Task.Factory.StartNew(() =>
{
Thread.CurrentThread.Name = "Task2's thread";
dictionary.GetOrAdd(key, CreateItem);
});
Log.AppendLine("Started Task2");
//Wait until both tasks are waiting to return factory value
var spinWait = new SpinWait();
while (_tasksStartedCount < 2)
spinWait.SpinOnce();
Log.AppendLine(
"Main - Releasing Sync so that tasks can proceed");
Sync.Release();
Task.WaitAll(task1, task2);
Log.AppendLine("Tasks done");
Console.WriteLine(Log.ToString());
Console.WriteLine(
"Dictionary contains string: '{0}'",
dictionary.First().Value);
Console.ReadKey();
}
private static string CreateItem(int key)
{
try
{
//Increase value so that Sync in Main will be released
Interlocked.Increment(ref _tasksStartedCount);
Log.AppendFormat(
"Thread '{0}' - In CreateItem, before Sync.Wait\n",
Thread.CurrentThread.Name);
Sync.Wait();
Log.AppendFormat(
"Thread '{0}' - In CreateItem, returning value: '{0}'.\n",
Thread.CurrentThread.Name);
return Thread.CurrentThread.Name;
}
finally
{
Log.AppendFormat(
"Thread '{0}' - In CreateItem, before Sync.Releaser\n",
Thread.CurrentThread.Name);
Sync.Release();
Log.AppendFormat(
"Thread '{0}' - In CreateItem, after Sync.Releaser\n",
Thread.CurrentThread.Name);
}
}
The output of this (with some variances) is:
Note that BOTH Funcs where executed but the first one finished was used for the value. Hence, if you pass a factory, don't make it expensive. You could of course use it in conjunction with e.g. Lazy, that is, let the Dictionary
hold a value of Lazy<T>
.
//Daniel