【问题标题】:ConcurrentDictionary.GetOrAdd when valueFactory has side-effects当 valueFactory 有副作用时 ConcurrentDictionary.GetOrAdd
【发布时间】:2014-07-23 19:49:45
【问题描述】:

我正在尝试通过为一些非常核心的函数引入缓存层来从我的数据库服务器卸载工作,这些函数将值插入数据库中的表并检索 id。这是在多线程环境中。

我的第一个方法是:

public class Cache {
      private Dictionary<string, Int64> i;

      public void Init() { /* init i with values from DB */ }

      public Int64 Get(string value)
         lock(i) {
            Int64 id;
            if (cache.i.TryGetValue(value, out id))
                return id;

            id = /* Insert to DB and retrieve ID */
            cache.i[value] = id;
            return id;
      }
 }

这有帮助。然而,线程仍然彼此等待很多。我想减少这个等待时间。我的第一个想法是使用ConcurrentDictionary.GetOrAdd(key, valueFactory)。这是行不通的,因为 valueFactory 可以被多次调用。

我已经结束了这种方法:

public class Cache
{
    private ConcurrentDictionary<string, Int64> i;

    public void Init() { /* init i with values from DB */ }

    public Int64 Get(string value)
    {
        Int64 id;
        if (i.TryGetValue(value, out id))
            return id;

        lock (i)
        {
            if (i.TryGetValue(value, out id))
                return id;

            id = /* Insert to DB and retrieve ID */
            i.TryAdd(value, id);
            return id;
        }
    }

有没有更好的方法来做到这一点?这甚至是线程安全的吗?

【问题讨论】:

  • 我不会锁定您尝试修改的资源。
  • @DanielA.White 为什么不呢?这是一个非常好的做法。
  • @Servy 你不知道 .net 框架是否可以锁定该引用。
  • @DanielA.White 我希望ConcurrentDictionary 的定义遵循您链接的文章中的建议,并且不会自我锁定,因为 MS 知道这样做是个坏主意所以。粗略检查一下它的定义让我相信它不会像预期的那样自我锁定。

标签: c# database multithreading concurrency concurrentdictionary


【解决方案1】:

您要做的是懒惰地创建一个对象,该对象只需要创建一次,然后在创建后由任意数量的线程访问。 Lazy 正是为此而设计的:

public class Cache
{
    private ConcurrentDictionary<string, Lazy<long>> i;

    public void Init() { /* init i with values from DB */ }

    public Int64 Get(string value)
    {
        return i.GetOrAdd(value, new Lazy<long>(() =>
            CreateDatabaseRecordAndGetId()))
            .Value;
    }

    private long CreateDatabaseRecordAndGetId()
    {
        throw new NotImplementedException();
    }
}

【讨论】:

  • 感谢您将我指向 Lazy。不需要 LazyThreadSafetyMode.ExecutionAndPublication 吗?
  • @KasperVidebæk 这是默认行为。基本上它希望一切都需要完全同步,除非你明确告诉它
  • 好的。谢谢。这种方法会比我目前的方法更安全或更快吗?
  • @KasperVidebæk 两者都是安全的。如果两个线程各自尝试添加一个新项目,则它们不需要互相等待来运行它们的初始化方法,这会更快。在您的情况下,只要一个线程正在初始化一个值,其他线程就不能初始化任何其他值。它也更清晰、更容易使用,因为它使用的是更高级的工具,这些工具准确地代表了您想要做的事情,因此不会有错误或误解的余地。
  • 我的缓存从 1.6GB 的内存占用变成了 2.3GB 之一。我编辑创建了一个不需要存储惰性对象的版本。你同意这行得通吗?
【解决方案2】:

仅供参考,在 Servy 的示例中,您会为每次调用 GetOrAdd 创建一个 Lazy 实例。现在,Lazy 的魔力仍然存在,您只需接到一个创建实例的Func 调用。但也许上面示例中 Lazy 的额外实例化解释了您在尝试时看到的内存增加。

如果你创建一个“双”lambda,你不会得到 Lazy 的多个实例化。

例如将此粘贴到控制台应用程序中,并比较下面有和没有x =&gt; new Lazy... 的实现:

public static class LazyEvaluationTesting
{
    private static readonly ConcurrentDictionary<int, CustomLazy<CacheableItem>>
        cacheableItemCache = new ConcurrentDictionary<int, CustomLazy<CacheableItem>>();

    private static CacheableItem RetrieveCacheableItem(int itemId)
    {
        Console.WriteLine("--RETRIEVE called\t ItemId [{0}] ThreadId [{1}]", itemId, Thread.CurrentThread.ManagedThreadId);
        return new CacheableItem
        {
            ItemId = itemId
        };
    }

    private static void GetCacheableItem(int itemId)
    {
        Console.WriteLine("GET called\t ItemId [{0}] ThreadId [{1}]", itemId, Thread.CurrentThread.ManagedThreadId);

        CacheableItem cacheableItem = cacheableItemCache
            .GetOrAdd(itemId,
                x => new CustomLazy<CacheableItem>(
                    () => RetrieveCacheableItem(itemId)
                )
            ).Value;

        //CacheableItem cacheableItem2 = cacheableItemCache
        //  .GetOrAdd(itemId,
        //      new CustomLazy<CacheableItem>(
        //          () => RetrieveCacheableItem(itemId)
        //      )
        //  ).Value;
    }

    public static void TestLazyEvaluation()
    {
        int[] itemIds = { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 };
        ParallelOptions options = new ParallelOptions
        {
            MaxDegreeOfParallelism = 75
        };

        Parallel.ForEach(itemIds, options, itemId =>
        {
            GetCacheableItem(itemId);
            GetCacheableItem(itemId);
            GetCacheableItem(itemId);
            GetCacheableItem(itemId);
            GetCacheableItem(itemId);
        });
    }

    private class CustomLazy<T> : Lazy<T> where T : class
    {
        public CustomLazy(Func<T> valueFactory)
            : base(valueFactory)
        {
            Console.WriteLine("-Lazy Constructor called  ThreadId [{0}]", Thread.CurrentThread.ManagedThreadId);
        }
    }

    private class CacheableItem
    {
        public int ItemId { get; set; }
    }
}

来源:Reed Copsey's Blog

【讨论】:

  • 你是对的,你没有得到多个 Lazy 的实例化,但是你可以得到多个 CustomLazy 的实例化。鉴于第二个 Lazy 被删除并且只有一个 Lazy 实例访问了它的 Value 属性,我看不出这有多好。
  • 嗯...我不确定你是如何得出这个结论的。在我的示例中,CustomLazy 继承自 Lazy,它只是在构造函数中输出一条消息,让您查看它是否被调用。它在其他方面就像 Lazy 一样。你真的运行了我的示例代码,还是只是在做一个扶手椅观察?因为当我运行它时,我只看到我的消息被输出了一次。
  • 我也猜你没有阅读我链接的博客条目?
  • 对不起,我的笔记不是很清楚,我把我想说的话弄混了。您的版本更好,因为它并不总是创建一个 Lazy 传递给 GetOrAdd。我只是想指出,由于竞态条件,可能会创建额外的 Lazy 实例,请参阅 pastebin.com/rvDry7ED 以获取演示这一点的代码。
  • 虽然接受回调的 GetOrAdd 版本不包含像 new Lazy() 这样的显式分配,但它确实传递了一个必须来自某个地方的对象(Func 类型)。最终如何编译/JITted 有一些灵活性,但请注意,第二个回调引用 itemId,因此在大多数实现中必须构造一个闭包。这显然有很多因素,但我不会说你的重写总是产生更少的分配。
猜你喜欢
  • 2012-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-03
  • 2019-03-03
相关资源
最近更新 更多