可以递归调用ConcurrentDictionary.GetOrAdd()吗?

时间:2016-11-14 13:33:47

标签: .net concurrentdictionary

ConcurrentDictionary.GetOrAdd(TKey, Func<TKey, TValue>)接受一个工厂函数,允许将项目的延迟实例化放入字典中。

定义一个自己调用GetOrAdd()的工厂函数是否安全,即在父母的上下文中调用GetOrAdd。 GetOrAdd()。

以下代码演示了模式;它确实有效,但是安全吗?

class Program
{
    static ConcurrentDictionary<string,object> __dict = new ConcurrentDictionary<string, object>();

    static void Main(string[] args)
    {
        Foo foo = GetOrAddFoo();
        Console.WriteLine(foo._name);
        Console.WriteLine(foo._bar._name);
        Console.ReadKey();
    }

    static Bar GetOrAddBar()
    {
        Console.WriteLine("GetOrAddBar: enter");
        Func<string,Bar> factoryFn = (x) => LoadBar(x);
        Bar bar = __dict.GetOrAdd("bar", factoryFn) as Bar;
        Console.WriteLine("GetOrAddBar: exit");
        return bar;
    }

    static Foo GetOrAddFoo()
    {
        Console.WriteLine("GetOrAddFoo: enter");
        Func<string,Foo> factoryFn = (x) => LoadFoo(x);
        Foo foo = __dict.GetOrAdd("foo", factoryFn) as Foo;
        Console.WriteLine("GetOrAddFoo: exit");
        return foo;
    }

    static Bar LoadBar(string name)
    {
        Bar bar =  new Bar();
        bar._name = name;
        return bar;
    }

    static Foo LoadFoo(string name)
    {
        Foo foo = new Foo();
        foo._name = name;
        foo._bar = GetOrAddBar();
        return foo;
    }

    public class Foo
    {
       public string _name;
       public Bar _bar;
    }

    public class Bar
    {
        public string _name;
    }
}

3 个答案:

答案 0 :(得分:3)

答案是肯定的,这是绝对安全的。仅当给定键存在时才调用值函数。以下是幕后故事的演练。

演练

首先,我们假设字典完全为空,为简单起见,我仅以数组格式显示键:

dictionary = []

在第一次GetOrAddFoo方法的首次执行中,“ Foo”键不存在 ,因此字典将调用值函数,在此调用中为{{1 }} 方法。字典在这里还是空的。

LoadFoo

dictionary = [] 内部调用LoadFoo,该方法检查并发现“ Bar”键不存在,因此调用GetOrAddBar值函数,并返回创建的“ Bar”条目。此时的字典如下所示:

LoadBar

此时字典中包含“ Bar”项。我们尚未完成dictionary = ["Bar"] 值函数,但现在已经完成。

LoadFoo

dictionary = ["Bar"] 方法重新获得控制权并返回要存储在字典中的LoadFoo对象。 Foo完成后,LoadFoo也可以完成。现在,我们的字典如下所示:

GetOrAddFoo

dictionary = ["Bar", "Foo"] 的未来呼叫

在随后对GetOrAddFoo的调用中,该字典已经具有GetOrAddFoo的条目,因此将不会调用其value函数,甚至不会调用Foo value函数。它立即返回。

Bar

但是,如果我们从字典中删除dictionary = ["Bar", "Foo"] 然后调用Bar,会发生什么情况?假设我们确实将其删除,则字典如下:

GetOrAddFoo

现在我们再次调用dictionary = ["Foo"] GetOrAddFoo仍然存在,因此字典将调用Foo值函数。因此,LoadFoo not 重新添加到字典中。我们的字典保持不变:

Bar

但是,如果我们直接调用dictionary = ["Foo"] ,那么我们将重新添加“ Bar”。

GetOrAddBar

在并发词典的Get-Or-Add方法的背景下

在后台,只要调用dictionary = ["Foo", "Bar"] 方法,就会首先检查给定密钥的存在。

如果此时不存在,则调用value函数。值函数返回后,将在字典上放置一个锁,以允许添加新条目。

在多线程世界中,我们可能有2个线程试图向字典添加相同的确切键。字典将运行value函数,然后尝试获取锁,并在获得锁后检查该键是否再次存在。

其原因是在锁定检索上,另一个线程可能会跳入并添加相同的密钥。原始线程一旦收到锁定,就需要检查这种情况,以免最终导致键冲突。

答案 1 :(得分:2)

反编译ConcurrentDictionary.GetOrAdd(TKey,Func)时,您将看到以下行:

TryAddInternal(key, valueFactory(key), updateIfExists: false, acquireLock: true, out value);

这意味着您在一个进程/线程中的呼叫时间表将是:

enter __dict.GetOrAdd("foo", factoryFn)
enter LoadFoo (from valueFactory(key) in line above)
enter GetOrAddBar
enter __dict.GetOrAdd("bar", factoryFn)
enter LoadBar
leave LoadBar
enter TryAddInternal with key "bar" 
acquire lock ( Monitor.Enter(tables.m_locks[lockNo], ref lockTaken); )
add key "bar" with appropriate value
release lock ( Monitor.Exit(tables.m_locks[lockNo]); )
leave TryAddInternal with key "bar" 
leave __dict.GetOrAdd("bar", factoryFn)
leave GetOrAddBar
leave LoadFoo
enter TryAddInternal with key "foo" 
acquire lock ( Monitor.Enter(tables.m_locks[lockNo], ref lockTaken); )
add key "foo" with appropriate value
release lock ( Monitor.Exit(tables.m_locks[lockNo]); )
leave __dict.GetOrAdd("foo", factoryFn)

您可以看到它将锁定两次并释放,并且在另一个进程之间,如果该进程已经创建了“ bar”,则可以跳转并创建“ foo”。是否安全取决于您。

并且当您使用相同的键递归调用时,大多数深度值将“获胜”,因为TryAddInternal中存在updateIfExists: false参数,因此任何后续调用都不会在该位置进行更改。还有out value参数,因此它将返回第一个插入的值,并且不会失败。

也很有趣:TryAddInternal不会锁定整个Dictionary,而只会锁定基于密钥的存储桶(词典的一部分)。这是性能的提高。

答案 2 :(得分:0)

调用valueFactory委托,它到达内部锁。 (基于MSDN)

因此,只要您的委托很简单并且不访问任何非线程安全的内容,它就会非常安全。我的意思是,如果你的方法只是创建一些类,那就没有危险。如果您的方法将尝试读取类的某些字段或执行某些锁定,则可能必须考虑如何解决死锁问题。

我认为这个内部锁是锁定__dict [key]