.NET 4.0中的ConcurrentDictionary<T,V>是线程安全的,但并非所有方法都是原子方法。

这指出:


...并非所有方法都是原子方法,特别是GetOrAddAddOrUpdate。传递给这些方法的用户委托是在字典的内部锁之外调用的。


示例问题:

委托方法有可能是对于给定的密钥,该命令执行了多次。对于我们正在使用的大多数实现,这不是理想的行为,并确认了上面MSDN链接中指出的问题。我们想要的是仅执行其中一个委托。

建议的解决方案:

我不得不使用这些方法的委托重载,这导致我不得不研究此问题。进一步。我偶然发现了这篇文章,该文章建议使用Lazy<T>类确保该委托仅被调用一次。考虑到这一点,我编写了以下扩展方法来掩盖在值中添加Lazy<T>包装器。

public static readonly ConcurrentDictionary<int, string> store =
    new ConcurrentDictionary<int, string>();

[TestMethod]
public void UnsafeConcurrentDictionaryTest()
{

    Thread t1 = new Thread(() =>
    {
        store.GetOrAdd(0, i =>
        {
            string msg = "Hello from t1";
            Trace.WriteLine(msg);
            Thread.SpinWait(10000);
            return msg;
        });
    });

    Thread t2 = new Thread(() =>
    {
        store.GetOrAdd(0, i =>
        {
            string msg = "Hello from t2";
            Trace.WriteLine(msg);
            Thread.SpinWait(10000);
            return msg;
        });
    });

    t1.Start();
    t2.Start();
    t1.Join();
    t2.Join();
}


测试解决方案:使用具有惰性值的ConcurrentDictionary执行上面的相同测试,将导致仅执行一次值委托(您可以看到“来自t1的Hello”或“来自t2的Hello”)!

public static V GetOrAdd<T, U, V>(this ConcurrentDictionary<T, U> dictionary, T key, Func<T, V> valueFactory)
where U : Lazy<V>
{
    U lazy = dictionary.GetOrAdd(key, (U)new Lazy<V>(() => valueFactory(key)));
    return lazy.Value;
}

public static V AddOrUpdate<T, U, V>(this ConcurrentDictionary<T, U> dictionary, T key, Func<T, V> addValueFactory, Func<T, V, V> updateValueFactory)
where U : Lazy<V>
{
    U lazy = dictionary.AddOrUpdate(key, 
                (U)new Lazy<V>(() => addValueFactory(key)), 
                (k, oldValue) => (U)new Lazy<V>(() => updateValueFactory(k, oldValue.Value)));
    return lazy.Value;
}


看来这种方法可以实现目标。

您如何看待这种方法?

评论

那么这是否意味着如果字典是由第三方公开的,并且未定义为>,那么您将无法执行该操作?

@Daryl我不明白你的问题。这些扩展方法旨在在Dictionary >上运行。如果第三方没有那样公开字典,那么您必须先将其转换为该类型,然后才能使用这些扩展方法。

您提议的解决方案定义为类型的字典,但是您接受的解决方案移至了>类型之一。我希望获得这种功能,而不必将字典更改为>。相反,我决定传递一个对象用作锁

可能值得注意的是,这种方法不是完全线程安全的,它只是减少了竞争某种条件的机会,但仍然可以发生。考虑一种情况,在这种情况下,第一个线程在分配第一个Lazy实例之后并在第二个Lazy实例之前调用Lazy.Value。下一次对Lazy.Value的调用将再次调用该方法。在非常高性能的应用程序中,这可能是一个真正的问题。

#1 楼



允许调用者提供类型参数U意味着允许他们使用Lazy<V>的子类,但这将不起作用,因为您的实现总是创建一个新的List<V>并将其转换为U。由于这意味着U必须始终是Lazy<V>,那么为什么不取消使用多余的类型自变量呢?现有方法的名称。为了让使用者使用您的而不是现有的方法,他们将需要通过您的静态类进行访问或使用显式类型参数。当消费者尝试将其用作类型推断的扩展方法时,这可能会导致细微的错误。

public static V GetOrAdd<T, V>(this ConcurrentDictionary<T, Lazy<V>> dictionary, T key, Func<T, V> valueFactory)




评论


\ $ \ begingroup \ $
感谢您的来信。关于类型U的要点。而且,我已经被微妙的bug咬住了。我同意我的重载过于类似于现有的GetOrAdd()方法。我会结合您的建议。
\ $ \ endgroup \ $
–亚当·斯派塞(Adam Spicer)
2011年4月23日在3:36