Cache that guarantees single creation of cached item

I want create a cache so that when an item doesn't exist, only one person who requested that item spends the time generating it, and others that request it at the same time will simply block until the result is cached by the first person. Here is a description of that scenario:

  • Thread 1 comes in and requests the cached data for DateA
  • Thread 1 sees that it isn't in the cache and starts the relatively long process of generating the information
  • Thread 2 comes in a sees that the information is currently being generated, waits on some kind of lock
  • Thread 3 also comes in and sees that the information is currently being generated, waits on some kind of lock
  • Thread 4 comes in and requests data for a different key that is already in the cache and doesn't wait at all
  • Thread 1 finishes the generation and updates the cache with the value
  • Both thread 2 and 3 wake up and get the result that is now in the cache
  • I was thinking I would have the cache in a ConcurrentDictionary with a DateTime storing just a date as the key like ConcurrentDictionary<DateTime, CustomImmutableObject> _cache;

    But I can't see how Thread 2 and 3 would be able to wait on something. Even if there was another ConcurrentDictionary that stored some kind of status flag, how would they know when Thread 1 finishes?

    Does anyone have any suggestions on how to approach developing such a cache?


    You could use ConcurrentDictionary<DateTime, Lazy<CustomImmutableObject>> with getting the object as follows:

    myDictionary
        .GetOrAdd(
            someDateTime,
            dt => new Lazy<CustomImmutableObject>(
                () => CreateMyCustomImmutableObject()
            )
        ).Value
    

    The guarantee made by Lazy<T> in (the default) thread-safe mode will ensure that initialization occurs only once and subsequent accesses prior to the value actually being instantiated will block.

    In multithreaded scenarios, the first thread to access the Value property of a thread-safe Lazy(Of T) object initializes it for all subsequent accesses on all threads, and all threads share the same data. Therefore, it does not matter which thread initializes the object, and race conditions are benign.

    See here for the details.

    Below is a test I wrote to ensure that I'm not spouting. This should persuade you that all is good:

    void Main()
    {
        var now=DateTime.UtcNow;
        var d=new ConcurrentDictionary<DateTime, Lazy<CustomImmutableObject>>();
        Action f=()=>{
            var val=d
                .GetOrAdd(
                    now,
                    dt => new Lazy<CustomImmutableObject>(
                        () => new CustomImmutableObject()
                    )
                ).Value;
            Console.WriteLine(val);
        };
        for(int i=0;i<10;++i)
        {
            (new Thread(()=>f())).Start();
        }
        Thread.Sleep(15000);
        Console.WriteLine("Finished");
    }
    
    class CustomImmutableObject
    {
        public CustomImmutableObject()
        {
            Console.WriteLine("CREATING");
            Thread.Sleep(10000);
        }
    }
    

    You could put a lock on the cache object each time anyone accesses it, and if there's a cache miss, do a Monitor.Enter on some object to indicate object creation is occurring, then release the first lock. When creation is done, use Monitor.Exit on the second lock object.

    Cache accesses usually lock on the main object, but creations lock on the second. If you want creations to happen in parallel, you could create one lock object in a dictionary, where the key is the same key for the cache.


    I came up with the following approach that seems to work, but at the cost of a lock on every read out of the cache. Is it safe to assume that only one person could ever add to cacheData for a given key at a time?

    static ConcurrentDictionary<DateTime, object> cacheAccess = new ConcurrentDictionary<DateTime, object>();
    static ConcurrentDictionary<DateTime, int> cacheData = new ConcurrentDictionary<DateTime, int>();
    
    static int GetValue(DateTime key)
    {
        var accessLock = cacheAccess.GetOrAdd(key, x =>  new object());
    
        lock (accessLock)
        {
            int resultValue;
            if (!cacheData.TryGetValue(key, out resultValue))
            {
                Console.WriteLine("Generating {0}", key);
                Thread.Sleep(5000);
                resultValue = (int)DateTime.Now.Ticks;
                if (!cacheData.TryAdd(key, resultValue))
                {
                    throw new InvalidOperationException("How can something else have added inside this lock?");
                }
            }
    
            return resultValue;
        }
    }
    
    
    static void Main(string[] args)
    {
        var keys = new[]{ DateTime.Now.Date, DateTime.Now.Date.AddDays(-1), DateTime.Now.Date.AddDays(1), DateTime.Now.Date.AddDays(2)};
        var rand = new Random();
    
        Parallel.For(0, 1000, (index) =>
            {
                var key = keys[rand.Next(keys.Length)];
    
                var value = GetValue(key);
    
                Console.WriteLine("Got {0} for key {1}", value, key);
            });
    }
    
    链接地址: http://www.djcxy.com/p/62996.html

    上一篇: 如何缓存热门查询,以避免stamedes和空白结果

    下一篇: 确保单个创建缓存项目的缓存