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

.net 可以递归调用ConcurrentDictionary.GetOrAdd()吗?,.net,concurrentdictionary,.net,Concurrentdictionary,接受工厂函数以允许将项的延迟实例化放入字典中 定义一个本身调用GetOrAdd()的工厂函数是否安全,即在“父”GetOrAdd()的上下文中调用GetOrAdd() 下面的代码演示了该模式;它看起来确实有效,但安全吗 class Program { static ConcurrentDictionary<string,object> __dict = new ConcurrentDictionary<string, object>(); static

接受工厂函数以允许将项的延迟实例化放入字典中

定义一个本身调用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;
    }
}
类程序
{
静态ConcurrentDictionary uu dict=新ConcurrentDictionary();
静态void Main(字符串[]参数)
{
Foo-Foo=GetOrAddFoo();
控制台写入线(foo.\u名称);
控制台写入线(foo.\u-bar.\u-name);
Console.ReadKey();
}
静态条getOradBar()
{
Console.WriteLine(“GetOrAddBar:enter”);
Func factoryFn=(x)=>加载条(x);
Bar Bar=uu dict.GetOrAdd(“Bar”,factoryFn)作为Bar;
Console.WriteLine(“GetOrAddBar:exit”);
返回杆;
}
静态Foo GetOrAddFoo()
{
Console.WriteLine(“GetOrAddFoo:enter”);
Func factoryFn=(x)=>LoadFoo(x);
Foo Foo=uu dict.GetOrAdd(“Foo”,factoryFn)作为Foo;
Console.WriteLine(“GetOrAddFoo:exit”);
返回foo;
}
静态条加载条(字符串名称)
{
条形=新条形();
条._name=名称;
返回杆;
}
静态Foo LoadFoo(字符串名称)
{
Foo-Foo=新的Foo();
foo._name=名称;
foo._bar=GetOrAddBar();
返回foo;
}
公开课Foo
{
公共字符串\u名称;
公共酒吧;;
}
公共类酒吧
{
公共字符串\u名称;
}
}

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

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


我假设这个内部锁是锁上的,答案是肯定的,它是完全安全的。仅当给定的键尚不存在时才调用值函数。下面是对引擎盖下发生的情况的演练

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

dictionary = []
在第一次执行
GetOrAddFoo
方法时,“Foo”键不存在,因此字典调用value函数,该函数在此调用中是
LoadFoo
方法。这里的字典还是空的

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

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

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

dictionary = ["Bar"]
dictionary = ["Bar", "Foo"]
dictionary = ["Foo"]
未来调用
GetOrAddFoo
在随后调用
GetOrAddFoo
时,字典已经有了
Foo
的条目,因此不会调用其value函数,甚至也不会调用
Bar
value函数。它立即返回

dictionary = ["Bar", "Foo"]
但是如果我们从字典中删除
Bar
,然后调用
GetOrAddFoo
,会发生什么呢?假设我们确实删除了它,让我们的字典如下所示:

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

dictionary = ["Foo"]
但是,如果我们直接调用
GetOrAddBar
,那么我们会在其中添加“Bar”

dictionary = ["Foo", "Bar"]
在并发字典的Get或Add方法的保护下 在引擎盖下,只要调用
GetOrAdd
方法,就会首先检查给定密钥的存在性

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

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


原因是在锁检索时,另一个线程可能会跳入并添加相同的密钥。原始线程在收到锁后需要检查此场景,以便不会导致密钥冲突。

当您反编译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
param,因此任何后续调用在它出现后都不会更改它。还有
out value
param,因此它将返回第一个插入的值,并且不会失败


同样有趣的是:
TryAddInternal
不会锁定整个字典,而只锁定一个基于键的存储桶(字典的一部分)。这是性能改进。

我认为这并不能正确回答这个问题,因为文档中指出“…valueFactory委托在锁之外调用