2011-11-02 80 views
0

我想要创建一个缓存,以便当某个项目不存在时,只有一个请求该项目的人花费时间来生成该项目,而另一些人同时请求它将会简单阻塞,直到结果被第一个人缓存。下面是场景的描述:确保单个创建缓存项目的缓存

  • 线程1进来并请求DateA
  • 缓存数据
  • 线程1看到它是不是在缓存中,并开始生成信息的相对较长的过程
  • 线程2是在一个看到当前正在被生成的信息,等待在某些类型的锁
  • 线程3还配备在并看到当前正在生成的信息,等待在某些类型的锁
  • 线程4进来并请求为已存在于高速缓存中的不同密钥提供数据并且根本不等待
  • 线程1完成生成并使用值更新高速缓存
  • 线程2和3都唤醒并获得结果,即现在在缓存

我想我会缓存在ConcurrentDictionary与一个DateTime存储只是一个日期作为像ConcurrentDictionary<DateTime, CustomImmutableObject> _cache;

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

有没有人有任何建议如何处理开发这样的缓存?

回答

4

你可以使用ConcurrentDictionary<DateTime, Lazy<CustomImmutableObject>>与得到的对象,如下所示:

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

通过Lazy<T>中(默认)的线程安全模式下进行将确保初始化担保只发生一次和前值的后续访问实际被实例化会阻止。

在多线程方案中,第一线程访问的对象初始化线程安全懒惰(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); 
    } 
} 
+0

谢谢!这看起来是一个好的解决方案。在relfector中查看懒惰它基本上是一个Tuple ,如果该值尚未创建,它将锁定该对象。有点类似于我在下面发布的两个字典(一个带有锁定对象和一个带有缓存值的对象),但更清洁 – BrandonAGr

+0

确实。就个人而言,我希望看到应用于ConcurrentDictionary的GetOrAdd方法的相同方法,但在并发使用中,可以多次调用添加委托,并且除了一种情况外,都会抛弃返回值。当然,使用懒惰可以缓解这一点。 – spender

+1

“我希望看到应用于ConcurrentDictionary的GetOrAdd方法的相同方法” - 此方法将允许在添加委托中调用的任意外部代码在锁定期间运行 - 这可能会导致难以调试错误(重入;长时间锁定)。恕我直言,目前的设计是最好的折衷。 – Joe

0

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

缓存访问通常锁定在主对象上,但创建锁定在第二个对象上。如果您希望并行创建,则可以在字典中创建一个锁对象,其中的密钥与高速缓存的密钥相同。

0

我想出了下面的办法,似乎工作,但在每一个锁的成本读取缓存的出来。假设只有一个人可以一次添加一个给定密钥的cacheData 是否安全?只要你重新检查任何对象,你正在寻找的锁内,你应该没问题缓存

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

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