当valueFactory有副作用时,ConcurrentDictionary.GetOrAdd

时间:2021-04-30 07:09:45

I'm trying to offload work from my database server by introducing a cache layer for some very central functions that insert a value to a table in the database and retrieves the id. This is in a multi-threaded environment.

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

My first approach was:

我的第一个方法是:

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;
      }
 }

This helped. However the threads still wait a lot for each other. I'd like to reduce this waiting time. My first thought was to use ConcurrentDictionary.GetOrAdd(key, valueFactory). This would not work because valueFactory could be called more than once.

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

I've wound up at this approach:

我已经结束了这种方法:

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;
        }
    }

Is there a better way of doing this? Is this even thread-safe?

有更好的方法吗?这是否是线程安全的?

2 个解决方案

#1


11  

What you're trying to do is lazily create an object that needs to be created no more than once and then accessed by any number of threads once created. Lazy is designed for exactly this:

你要做的是懒洋洋地创建一个对象,需要创建一次不超过一次,然后创建后由任意数量的线程访问。 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();
    }
}

#2


0  

Just FYI, in Servy's example you get an instance of Lazy created for every call to GetOrAdd. Now, the magic of Lazy still happens, and you only get one call to your Func that creates your instance. But maybe the extra instantiations of Lazy in the above example explain the memory increase you saw when you tried it.

仅供参考,在Servy的示例中,您将获得为每次调用GetOrAdd创建的Lazy实例。现在,Lazy的神奇之处仍然存在,而且你只能调用一个创建你的实例的Func。但也许在上面的例子中,Lazy的额外实例解释了你在尝试时看到的内存增加。

If you create a "double" lambda, you don't get multiple instantiations of Lazy.

如果您创建一个“double”lambda,则不会获得Lazy的多个实例。

E.g. paste this into a console app and compare the implementation with and without the x => new Lazy... below:

例如。将其粘贴到控制台应用程序中,并使用以下x => 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; }
    }
}

Source: Reed Copsey's Blog

资料来源:Reed Copsey的博客

#1


11  

What you're trying to do is lazily create an object that needs to be created no more than once and then accessed by any number of threads once created. Lazy is designed for exactly this:

你要做的是懒洋洋地创建一个对象,需要创建一次不超过一次,然后创建后由任意数量的线程访问。 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();
    }
}

#2


0  

Just FYI, in Servy's example you get an instance of Lazy created for every call to GetOrAdd. Now, the magic of Lazy still happens, and you only get one call to your Func that creates your instance. But maybe the extra instantiations of Lazy in the above example explain the memory increase you saw when you tried it.

仅供参考,在Servy的示例中,您将获得为每次调用GetOrAdd创建的Lazy实例。现在,Lazy的神奇之处仍然存在,而且你只能调用一个创建你的实例的Func。但也许在上面的例子中,Lazy的额外实例解释了你在尝试时看到的内存增加。

If you create a "double" lambda, you don't get multiple instantiations of Lazy.

如果您创建一个“double”lambda,则不会获得Lazy的多个实例。

E.g. paste this into a console app and compare the implementation with and without the x => new Lazy... below:

例如。将其粘贴到控制台应用程序中,并使用以下x => 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; }
    }
}

Source: Reed Copsey's Blog

资料来源:Reed Copsey的博客