缓存,保证单个创建缓存项

时间:2011-11-02 00:33:21

标签: c# .net caching concurrency

我想创建一个缓存,这样当一个项目不存在时,只有一个请求该项目的人花时间生成它,而其他人同时请求它将直接阻塞,直到结果缓存为第一个人。以下是该场景的描述:

  • 线程1进入并请求DateA的缓存数据
  • 线程1看到它不在缓存中并且启动时间相对较长 生成信息的过程
  • 线程2看到当前正在生成信息,等待某种锁定
  • 线程3也进来,看到当前正在生成信息,等待某种锁定
  • 线程4进入并请求已经在缓存中并且根本不等待的其他密钥的数据
  • 线程1完成生成并使用值
  • 更新缓存
  • 线程2和3都唤醒并获取现在在缓存中的结果

我原以为我会在ConcurrentDictionary中使用DateTime来存储日期作为关键字,如ConcurrentDictionary<DateTime, CustomImmutableObject> _cache;

但是我无法看到线程2和3如何能够等待某些东西。即使存在另一个存储某种状态标志的ConcurrentDictionary,它们如何知道线程1何时完成?

有没有人对如何开发此类缓存有任何建议?

4 个答案:

答案 0 :(得分:4)

您可以使用ConcurrentDictionary<DateTime, Lazy<CustomImmutableObject>>获取对象,如下所示:

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

Lazy<T>在(默认)线程安全模式下所做的保证将确保初始化仅发生一次,并且在实际实例化的值之前的后续访问将阻止。

  

在多线程方案中,第一个访问线程安全的Lazy(Of T)对象的Value属性的线程会为所有线程上的所有后续访问初始化它,并且所有线程共享相同的数据。因此,哪个线程初始化对象并且竞争条件是良性的并不重要。

有关详细信息,请参阅here

下面是我写的一个测试,以确保我不会喷出。这应该能说服你一切都好:

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

答案 1 :(得分:0)

每次有人访问缓存对象时都可以放置lock,如果有缓存未命中,请在某个对象上执行Monitor.Enter以指示正在创建对象,然后释放第一个锁。创建完成后,在第二个锁定对象上使用Monitor.Exit

缓存访问通常会锁定主对象,但创建会锁定第二个。如果您希望创建并行发生,则可以在字典中创建一个锁定对象,其中键是缓存的相同键。

答案 2 :(得分:0)

我提出了以下似乎有效的方法,但代价是锁定每次读取缓存。假设只有一个人可以添加到cacheData是安全的  对于一次给定的密钥?

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

答案 3 :(得分:0)

通常情况下会这样:

public static object GetItem(DateTime dt)
{
    // Check to see if the cache already has my item
    if (_dictionary.ContainsKey(dt))
        return _dictionary[dt];

    lock (_lock)
    {
        // Check the cache AGAIN in case a previous thread inserted our value
        if (_dictionary.ContainsKey(dt))
            return _dictionary[dt];

        // Add the generate object to the dictionary
        _dictionary.Add(dt, GenerateMyObject(dt));
    }
}
private static Dictionary<DateTime, object> _dictionary = new Dictionary<DateTime, object>();
private static object _lock = new object();

只要你重新检查你在锁内找到的任何对象的缓存,你应该没问题。