背景:
我维护多个应用程序的WinForms和类库,要么可能或已经不从缓存中受益。 我也知道的缓存应用程序块和System.Web.Caching命名空间(其中,从我收集的,也完全可以使用外ASP.NET)。
我发现,虽然上述两个班的在技术上的意义上,单独的方法是同步的“线程安全的”,他们并不真正似乎特别设计以及多线程的情况。 特别是,他们没有实现GetOrAdd
方法类似于一个新ConcurrentDictionary
类在.NET 4.0中。
我认为这种方法是一种原始的缓存/查找功能,显然框架设计者意识到这也太-这就是为什么在并发集合存在的方法。 然而,除此之外,我没有使用.NET 4.0中的生产应用程序可事实,字典是不是一个成熟的缓存 - 它没有像过期的,永久/分布式存储等功能
为什么这是很重要的:
在“富客户端”应用程序相当典型的设计(甚至一些web应用程序)是开始,一旦应用程序启动时预先加载缓存,如果客户端请求尚未加载的数据阻塞(后来缓存以供将来使用)。 如果用户是通过他的工作流程快速犁地,或者网络连接速度很慢,这是不寻常,在所有客户端与预加载的竞争,它真的没有很大的意义,要求相同的数据两次,尤其是如果该请求是相对昂贵的。
所以,我似乎只剩下几个同样糟糕的选择:
澄清:实施例时间轴
说,一个应用程序启动时,它需要加载3点的数据集,其中每个需要10秒来加载。 考虑以下两个时间线:
00:00 - Start loading Dataset 1
00:10 - Start loading Dataset 2
00:19 - User asks for Dataset 2
在上述情况下,如果我们不使用任何类型的同步,用户必须等待了整整10秒,将在1秒钟内提供的数据,因为代码会看到该项目尚未加载到缓存中并尝试重新装入。
00:00 - Start loading Dataset 1
00:10 - Start loading Dataset 2
00:11 - User asks for Dataset 1
在这种情况下,用户被要求这是已经在缓存中的数据。 但是,如果我们连续访问缓存,他将不得不等待,另有9秒无缘无故,因为缓存管理器(无论是)无特定项目的意识被要求,只有“东西”是被请求和“东西”正在进行中。
问题:
是否有.NET(预4.0), 它们实现这样的原子操作,因为人们可能从一个线程安全的缓存指望任何缓存库?
或者,有没有一些方法来扩展现有的“线程安全”的高速缓存来支持这样的操作, 而无需序列化到高速缓存(这违背首先使用一个线程安全的实现的目的)访问? 我怀疑有,但也许我只是累了,忽略了一个显而易见的解决办法。
还是......有别的我失踪? 难道仅仅是标准的做法,让两个相互竞争的线程steamroll对方,如果他们碰巧两者请求相同的项目,在同一时间,在第一次或到期后?
我知道你的痛苦,因为我的建筑师之一Dedoose 。 我搞砸周围有很多的缓存库,并最终建立这一个多灾难之后。 在一个假设这个缓存管理器是由该类存储所有集合实现一个接口来获得一个GUID作为每个对象的“ID”属性。 作为,这是一个RIA它包括了很多的方法来添加/更新/删除从这些集合项目。
这里是我的CollectionCacheManager
public class CollectionCacheManager
{
private static readonly object _objLockPeek = new object();
private static readonly Dictionary<String, object> _htLocksByKey = new Dictionary<string, object>();
private static readonly Dictionary<String, CollectionCacheEntry> _htCollectionCache = new Dictionary<string, CollectionCacheEntry>();
private static DateTime _dtLastPurgeCheck;
public static List<T> FetchAndCache<T>(string sKey, Func<List<T>> fGetCollectionDelegate) where T : IUniqueIdActiveRecord
{
List<T> colItems = new List<T>();
lock (GetKeyLock(sKey))
{
if (_htCollectionCache.Keys.Contains(sKey) == true)
{
CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey];
colItems = (List<T>) objCacheEntry.Collection;
objCacheEntry.LastAccess = DateTime.Now;
}
else
{
colItems = fGetCollectionDelegate();
SaveCollection<T>(sKey, colItems);
}
}
List<T> objReturnCollection = CloneCollection<T>(colItems);
return objReturnCollection;
}
public static List<Guid> FetchAndCache(string sKey, Func<List<Guid>> fGetCollectionDelegate)
{
List<Guid> colIds = new List<Guid>();
lock (GetKeyLock(sKey))
{
if (_htCollectionCache.Keys.Contains(sKey) == true)
{
CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey];
colIds = (List<Guid>)objCacheEntry.Collection;
objCacheEntry.LastAccess = DateTime.Now;
}
else
{
colIds = fGetCollectionDelegate();
SaveCollection(sKey, colIds);
}
}
List<Guid> colReturnIds = CloneCollection(colIds);
return colReturnIds;
}
private static List<T> GetCollection<T>(string sKey) where T : IUniqueIdActiveRecord
{
List<T> objReturnCollection = null;
if (_htCollectionCache.Keys.Contains(sKey) == true)
{
CollectionCacheEntry objCacheEntry = null;
lock (GetKeyLock(sKey))
{
objCacheEntry = _htCollectionCache[sKey];
objCacheEntry.LastAccess = DateTime.Now;
}
if (objCacheEntry.Collection != null && objCacheEntry.Collection is List<T>)
{
objReturnCollection = CloneCollection<T>((List<T>)objCacheEntry.Collection);
}
}
return objReturnCollection;
}
public static void SaveCollection<T>(string sKey, List<T> colItems) where T : IUniqueIdActiveRecord
{
CollectionCacheEntry objCacheEntry = new CollectionCacheEntry();
objCacheEntry.Key = sKey;
objCacheEntry.CacheEntry = DateTime.Now;
objCacheEntry.LastAccess = DateTime.Now;
objCacheEntry.LastUpdate = DateTime.Now;
objCacheEntry.Collection = CloneCollection(colItems);
lock (GetKeyLock(sKey))
{
_htCollectionCache[sKey] = objCacheEntry;
}
}
public static void SaveCollection(string sKey, List<Guid> colIDs)
{
CollectionCacheEntry objCacheEntry = new CollectionCacheEntry();
objCacheEntry.Key = sKey;
objCacheEntry.CacheEntry = DateTime.Now;
objCacheEntry.LastAccess = DateTime.Now;
objCacheEntry.LastUpdate = DateTime.Now;
objCacheEntry.Collection = CloneCollection(colIDs);
lock (GetKeyLock(sKey))
{
_htCollectionCache[sKey] = objCacheEntry;
}
}
public static void UpdateCollection<T>(string sKey, List<T> colItems) where T : IUniqueIdActiveRecord
{
lock (GetKeyLock(sKey))
{
if (_htCollectionCache.ContainsKey(sKey) == true)
{
CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey];
objCacheEntry.LastAccess = DateTime.Now;
objCacheEntry.LastUpdate = DateTime.Now;
objCacheEntry.Collection = new List<T>();
//Clone the collection before insertion to ensure it can't be touched
foreach (T objItem in colItems)
{
objCacheEntry.Collection.Add(objItem);
}
_htCollectionCache[sKey] = objCacheEntry;
}
else
{
SaveCollection<T>(sKey, colItems);
}
}
}
public static void UpdateItem<T>(string sKey, T objItem) where T : IUniqueIdActiveRecord
{
lock (GetKeyLock(sKey))
{
if (_htCollectionCache.ContainsKey(sKey) == true)
{
CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey];
List<T> colItems = (List<T>)objCacheEntry.Collection;
colItems.RemoveAll(o => o.Id == objItem.Id);
colItems.Add(objItem);
objCacheEntry.Collection = colItems;
objCacheEntry.LastAccess = DateTime.Now;
objCacheEntry.LastUpdate = DateTime.Now;
}
}
}
public static void UpdateItems<T>(string sKey, List<T> colItemsToUpdate) where T : IUniqueIdActiveRecord
{
lock (GetKeyLock(sKey))
{
if (_htCollectionCache.ContainsKey(sKey) == true)
{
CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey];
List<T> colCachedItems = (List<T>)objCacheEntry.Collection;
foreach (T objItem in colItemsToUpdate)
{
colCachedItems.RemoveAll(o => o.Id == objItem.Id);
colCachedItems.Add(objItem);
}
objCacheEntry.Collection = colCachedItems;
objCacheEntry.LastAccess = DateTime.Now;
objCacheEntry.LastUpdate = DateTime.Now;
}
}
}
public static void RemoveItemFromCollection<T>(string sKey, T objItem) where T : IUniqueIdActiveRecord
{
lock (GetKeyLock(sKey))
{
List<T> objCollection = GetCollection<T>(sKey);
if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) > 0)
{
objCollection.RemoveAll(o => o.Id == objItem.Id);
UpdateCollection<T>(sKey, objCollection);
}
}
}
public static void RemoveItemsFromCollection<T>(string sKey, List<T> colItemsToAdd) where T : IUniqueIdActiveRecord
{
lock (GetKeyLock(sKey))
{
Boolean bCollectionChanged = false;
List<T> objCollection = GetCollection<T>(sKey);
foreach (T objItem in colItemsToAdd)
{
if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) > 0)
{
objCollection.RemoveAll(o => o.Id == objItem.Id);
bCollectionChanged = true;
}
}
if (bCollectionChanged == true)
{
UpdateCollection<T>(sKey, objCollection);
}
}
}
public static void AddItemToCollection<T>(string sKey, T objItem) where T : IUniqueIdActiveRecord
{
lock (GetKeyLock(sKey))
{
List<T> objCollection = GetCollection<T>(sKey);
if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) == 0)
{
objCollection.Add(objItem);
UpdateCollection<T>(sKey, objCollection);
}
}
}
public static void AddItemsToCollection<T>(string sKey, List<T> colItemsToAdd) where T : IUniqueIdActiveRecord
{
lock (GetKeyLock(sKey))
{
List<T> objCollection = GetCollection<T>(sKey);
Boolean bCollectionChanged = false;
foreach (T objItem in colItemsToAdd)
{
if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) == 0)
{
objCollection.Add(objItem);
bCollectionChanged = true;
}
}
if (bCollectionChanged == true)
{
UpdateCollection<T>(sKey, objCollection);
}
}
}
public static void PurgeCollectionByMaxLastAccessInMinutes(int iMinutesSinceLastAccess)
{
DateTime dtThreshHold = DateTime.Now.AddMinutes(iMinutesSinceLastAccess * -1);
if (_dtLastPurgeCheck == null || dtThreshHold > _dtLastPurgeCheck)
{
lock (_objLockPeek)
{
CollectionCacheEntry objCacheEntry;
List<String> colKeysToRemove = new List<string>();
foreach (string sCollectionKey in _htCollectionCache.Keys)
{
objCacheEntry = _htCollectionCache[sCollectionKey];
if (objCacheEntry.LastAccess < dtThreshHold)
{
colKeysToRemove.Add(sCollectionKey);
}
}
foreach (String sKeyToRemove in colKeysToRemove)
{
_htCollectionCache.Remove(sKeyToRemove);
}
}
_dtLastPurgeCheck = DateTime.Now;
}
}
public static void ClearCollection(String sKey)
{
lock (GetKeyLock(sKey))
{
lock (_objLockPeek)
{
if (_htCollectionCache.ContainsKey(sKey) == true)
{
_htCollectionCache.Remove(sKey);
}
}
}
}
#region Helper Methods
private static object GetKeyLock(String sKey)
{
//Ensure even if hell freezes over this lock exists
if (_htLocksByKey.Keys.Contains(sKey) == false)
{
lock (_objLockPeek)
{
if (_htLocksByKey.Keys.Contains(sKey) == false)
{
_htLocksByKey[sKey] = new object();
}
}
}
return _htLocksByKey[sKey];
}
private static List<T> CloneCollection<T>(List<T> colItems) where T : IUniqueIdActiveRecord
{
List<T> objReturnCollection = new List<T>();
//Clone the list - NEVER return the internal cache list
if (colItems != null && colItems.Count > 0)
{
List<T> colCachedItems = (List<T>)colItems;
foreach (T objItem in colCachedItems)
{
objReturnCollection.Add(objItem);
}
}
return objReturnCollection;
}
private static List<Guid> CloneCollection(List<Guid> colIds)
{
List<Guid> colReturnIds = new List<Guid>();
//Clone the list - NEVER return the internal cache list
if (colIds != null && colIds.Count > 0)
{
List<Guid> colCachedItems = (List<Guid>)colIds;
foreach (Guid gId in colCachedItems)
{
colReturnIds.Add(gId);
}
}
return colReturnIds;
}
#endregion
#region Admin Functions
public static List<CollectionCacheEntry> GetAllCacheEntries()
{
return _htCollectionCache.Values.ToList();
}
public static void ClearEntireCache()
{
_htCollectionCache.Clear();
}
#endregion
}
public sealed class CollectionCacheEntry
{
public String Key;
public DateTime CacheEntry;
public DateTime LastUpdate;
public DateTime LastAccess;
public IList Collection;
}
下面是我如何使用它的例子:
public static class ResourceCacheController
{
#region Cached Methods
public static List<Resource> GetResourcesByProject(Guid gProjectId)
{
String sKey = GetCacheKeyProjectResources(gProjectId);
List<Resource> colItems = CollectionCacheManager.FetchAndCache<Resource>(sKey, delegate() { return ResourceAccess.GetResourcesByProject(gProjectId); });
return colItems;
}
#endregion
#region Cache Dependant Methods
public static int GetResourceCountByProject(Guid gProjectId)
{
return GetResourcesByProject(gProjectId).Count;
}
public static List<Resource> GetResourcesByIds(Guid gProjectId, List<Guid> colResourceIds)
{
if (colResourceIds == null || colResourceIds.Count == 0)
{
return null;
}
return GetResourcesByProject(gProjectId).FindAll(objRes => colResourceIds.Any(gId => objRes.Id == gId)).ToList();
}
public static Resource GetResourceById(Guid gProjectId, Guid gResourceId)
{
return GetResourcesByProject(gProjectId).SingleOrDefault(o => o.Id == gResourceId);
}
#endregion
#region Cache Keys and Clear
public static void ClearCacheProjectResources(Guid gProjectId)
{ CollectionCacheManager.ClearCollection(GetCacheKeyProjectResources(gProjectId));
}
public static string GetCacheKeyProjectResources(Guid gProjectId)
{
return string.Concat("ResourceCacheController.ProjectResources.", gProjectId.ToString());
}
#endregion
internal static void ProcessDeleteResource(Guid gProjectId, Guid gResourceId)
{
Resource objRes = GetResourceById(gProjectId, gResourceId);
if (objRes != null)
{ CollectionCacheManager.RemoveItemFromCollection(GetCacheKeyProjectResources(gProjectId), objRes);
}
}
internal static void ProcessUpdateResource(Resource objResource)
{
CollectionCacheManager.UpdateItem(GetCacheKeyProjectResources(objResource.Id), objResource);
}
internal static void ProcessAddResource(Guid gProjectId, Resource objResource)
{
CollectionCacheManager.AddItemToCollection(GetCacheKeyProjectResources(gProjectId), objResource);
}
}
这里是有问题的接口:
public interface IUniqueIdActiveRecord
{
Guid Id { get; set; }
}
希望这可以帮助,我经历了地狱和背部几次,终于在此作为解决办法,对于我们这是一个天赐良机,但我不能保证它是完美的,只是我们还没有发现一个问题,但。
它看起来像.NET 4.0并发收集利用上下文切换之前分拆新的同步原语,如果资源被迅速释放。 所以他们仍然锁定,只是在一个更为机会主义的方式。 如果你认为你的数据检索逻辑是比时间片短,那么它看起来这将是非常有益的。 但你提到的网络,这让我觉得这并不适用。
我会等到你有一个适当的简单的,同步的解决方案,并测量假设你将有相关的并发性能问题之前的表现和行为。
如果你真的关心高速缓存争,你可以利用现有的基础设施的缓存,它在逻辑上划分成多个区域。 然后独立地同步访问每个区域。
一个例子的策略,如果你的数据集由在数字ID键的项目,你想你的缓存划分为10个区域,你可以(MOD 10)的ID,以确定他们是在哪个地区,你会保持一个数组10个对象的锁定上。 所有的代码可以用于可变数量的区域,这可以通过配置进行设置,或者在应用程序启动取决于你预测/打算高速缓存项的总数来确定被写入。
如果你的缓存命中异常方式键,你就必须拿出一些自定义的启发式划分缓存。
(根据注释) 更新 :嗯,这真的很好玩。 我认为以下大约是细粒度锁,你可以足不出户完全疯了(或维持/同步锁的字典每个高速缓存键)希望。 我没有测试过所以有可能是错误的,但这个想法应该加以说明。 跟踪请求ID的列表,然后用它来决定是否需要对自己获得的项目,或者如果你只是需要等待前一个请求的完成。 等待(和缓存插入)与紧密作用域螺纹阻挡和使用信令同步Wait
和PulseAll
。 所请求的ID列表访问与紧密范围的同步ReaderWriterLockSlim
。
这是一个只读缓存。 如果你做创建/更新/删除,你必须确保你从删除的ID requestedIds
一旦他们收到(调用之前Monitor.PulseAll(_cache)
你要添加其他try..finally
和收购在_requestedIdsLock
写锁)。 此外,与创建/更新/删除,管理缓存最简单的方法是仅仅从删除现有项目_cache
如果/当底层创建/更新/删除操作成功。
(糟糕,见下文更新2。)
public class Item
{
public int ID { get; set; }
}
public class AsyncCache
{
protected static readonly Dictionary<int, Item> _externalDataStoreProxy = new Dictionary<int, Item>();
protected static readonly Dictionary<int, Item> _cache = new Dictionary<int, Item>();
protected static readonly HashSet<int> _requestedIds = new HashSet<int>();
protected static readonly ReaderWriterLockSlim _requestedIdsLock = new ReaderWriterLockSlim();
public Item Get(int id)
{
// if item does not exist in cache
if (!_cache.ContainsKey(id))
{
_requestedIdsLock.EnterUpgradeableReadLock();
try
{
// if item was already requested by another thread
if (_requestedIds.Contains(id))
{
_requestedIdsLock.ExitUpgradeableReadLock();
lock (_cache)
{
while (!_cache.ContainsKey(id))
Monitor.Wait(_cache);
// once we get here, _cache has our item
}
}
// else, item has not yet been requested by a thread
else
{
_requestedIdsLock.EnterWriteLock();
try
{
// record the current request
_requestedIds.Add(id);
_requestedIdsLock.ExitWriteLock();
_requestedIdsLock.ExitUpgradeableReadLock();
// get the data from the external resource
#region fake implementation - replace with real code
var item = _externalDataStoreProxy[id];
Thread.Sleep(10000);
#endregion
lock (_cache)
{
_cache.Add(id, item);
Monitor.PulseAll(_cache);
}
}
finally
{
// let go of any held locks
if (_requestedIdsLock.IsWriteLockHeld)
_requestedIdsLock.ExitWriteLock();
}
}
}
finally
{
// let go of any held locks
if (_requestedIdsLock.IsUpgradeableReadLockHeld)
_requestedIdsLock.ExitReadLock();
}
}
return _cache[id];
}
public Collection<Item> Get(Collection<int> ids)
{
var notInCache = ids.Except(_cache.Keys);
// if some items don't exist in cache
if (notInCache.Count() > 0)
{
_requestedIdsLock.EnterUpgradeableReadLock();
try
{
var needToGet = notInCache.Except(_requestedIds);
// if any items have not yet been requested by other threads
if (needToGet.Count() > 0)
{
_requestedIdsLock.EnterWriteLock();
try
{
// record the current request
foreach (var id in ids)
_requestedIds.Add(id);
_requestedIdsLock.ExitWriteLock();
_requestedIdsLock.ExitUpgradeableReadLock();
// get the data from the external resource
#region fake implementation - replace with real code
var data = new Collection<Item>();
foreach (var id in needToGet)
{
var item = _externalDataStoreProxy[id];
data.Add(item);
}
Thread.Sleep(10000);
#endregion
lock (_cache)
{
foreach (var item in data)
_cache.Add(item.ID, item);
Monitor.PulseAll(_cache);
}
}
finally
{
// let go of any held locks
if (_requestedIdsLock.IsWriteLockHeld)
_requestedIdsLock.ExitWriteLock();
}
}
if (requestedIdsLock.IsUpgradeableReadLockHeld)
_requestedIdsLock.ExitUpgradeableReadLock();
var waitingFor = notInCache.Except(needToGet);
// if any remaining items were already requested by other threads
if (waitingFor.Count() > 0)
{
lock (_cache)
{
while (waitingFor.Count() > 0)
{
Monitor.Wait(_cache);
waitingFor = waitingFor.Except(_cache.Keys);
}
// once we get here, _cache has all our items
}
}
}
finally
{
// let go of any held locks
if (_requestedIdsLock.IsUpgradeableReadLockHeld)
_requestedIdsLock.ExitReadLock();
}
}
return new Collection<Item>(ids.Select(id => _cache[id]).ToList());
}
}
更新2:
我误解UpgradeableReadLock ...只有一个线程的行为,同时可以容纳一个UpgradeableReadLock。 所以,上面的需要重构只抢到读锁开始,并完全放弃他们,并添加项目时,获得一个完整的写锁_requestedIds
。
我实现了一个名为MemoryCacheT一个简单的库。 它是在GitHub上和的NuGet 。 它基本上存储项目在ConcurrentDictionary,你可以添加项目时指定到期策略。 任何反馈,审核,建议是值得欢迎的。
最后想出了一个可行的解决办法这是借助于在评论一番对话。 我所做的就是创建一个包装,它是使用任何标准的缓存库为后盾缓存部分实现的抽象基类(只需要实现Contains
, Get
, Put
,和Remove
方法)。 目前我使用的EntLib缓存应用程序块是什么,以及它花了一段时间才能得到这个启动和运行,因为该库的某些方面...好...不是深思熟虑的。
无论如何,总码已接近1K线,所以我不打算在这里发表了整个事情,但基本思路是:
拦截对所有呼叫Get
, Put/Add
和Remove
方法。
而不是增加原来的项目,加上其含有一个“入口”项目ManualResetEvent
除了Value
属性。 按今天给我上前面一个问题了一些建议,进入实施倒计时锁,每当项收购时递增,每当它被释放递减。 无论是装载程序和所有未来查找参加倒计时锁存器,所以当反降为零,该数据是保证供应, ManualResetEvent
是为了节约资源破坏。
当条目必须是延迟加载,该条目被创建并添加为后盾缓存向右走,在一个unsignaled状态的事件。 要么新的后续调用GetOrAdd
方法或拦截的Get
方法,会发现这个条目,并在事件或者等待(如果该事件存在),或者立即返回关联的值(如果事件不存在)。
该Put
方法将不活动的项目; 这些看起来是一样的,作为其延迟加载已经完成的条目。
由于GetOrAdd
仍然实现了Get
后跟可选Put
,这种方法是同步的(连载)对Put
及Remove
的方法,但只补充不完整的项目,不是懒惰的负载的整个过程。 在Get
方法没有序列号; 有效地将整个界面就像一个自动读写锁。
它仍然是一个进展中的工作,但我已经通过了十几个单元测试运行它,它似乎持股待涨。 它正确地表现为两个问题中描述的场景。 换一种说法:
这也给我的想法是,当你开始延迟加载,然后立即从缓存中删除此键最直观的结果; 最初请求值的线程将获得真正的价值,但是去除后随时请求相同的密钥的任何其他线程将得不到任何回报。( null
),并立即返回。
所有的一切,我与它很高兴。 我仍然希望有说这样做是为了我的图书馆,但我想,如果你想要的东西做对......嗯,你知道的。