danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

ConcurrentDictionary.GetOrAdd - Func not exclusively accessed

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

View Comments