.NET的线程安全缓存库

时间:2021-07-09 00:40:17

Background:

背景:

I maintain several Winforms apps and class libraries that either could or already do benefit from caching. I'm also aware of the Caching Application Block and the System.Web.Caching namespace (which, from what I've gathered, is perfectly OK to use outside ASP.NET).

我维护了几个可以或已经从缓存中受益的Winforms应用程序和类库。我也知道缓存应用程序块和System.Web.Caching命名空间(根据我收集的内容,在ASP.NET之外使用它是完全可以的)。

I've found that, although both of the above classes are technically "thread safe" in the sense that individual methods are synchronized, they don't really seem to be designed particularly well for multi-threaded scenarios. Specifically, they don't implement a GetOrAdd method similar to the one in the new ConcurrentDictionary class in .NET 4.0.

我发现,尽管上述两个类在技术上都是“线程安全的”,因为单个方法是同步的,但它们似乎并不是特别适用于多线程场景。具体来说,它们没有实现类似于.NET 4.0中新的ConcurrentDictionary类中的GetOrAdd方法。

I consider such a method to be a primitive for caching/lookup functionality, and obviously the Framework designers realized this too - that's why the methods exist in the concurrent collections. However, aside from the fact that I'm not using .NET 4.0 in production apps yet, a dictionary is not a full-fledged cache - it doesn't have features like expirations, persistent/distributed storage, etc.

我认为这种方法是缓存/查找功能的原始方法,显然框架设计者也意识到了这一点 - 这就是为什么方法存在于并发集合中的原因。但是,除了我还没有在生产应用程序中使用.NET 4.0这一事实,字典不是一个完整的缓存 - 它没有像过期,持久/分布式存储等功能。


Why this is important:

为什么这很重要:

A fairly typical design in a "rich client" app (or even some web apps) is to start pre-loading a cache as soon as the app starts, blocking if the client requests data that is not yet loaded (subsequently caching it for future use). If the user is plowing through his workflow quickly, or if the network connection is slow, it's not unusual at all for the client to be competing with the preloader, and it really doesn't make a lot of sense to request the same data twice, especially if the request is relatively expensive.

“富客户端”应用程序(甚至某些Web应用程序)中的一个相当典型的设计是在应用程序启动时立即开始预加载缓存,阻止客户端请求尚未加载的数据(随后将其缓存以备将来使用)使用)。如果用户正在快速浏览他的工作流程,或者网络连接速度很慢,那么客户端与预加载器竞争并不罕见,两次请求相同的数据真的没有多大意义。 ,特别是如果请求相对昂贵。

So I seem to be left with a few equally lousy options:

所以我似乎留下了一些同样糟糕的选择:

  • Don't try to make the operation atomic at all, and risk the data being loaded twice (and possibly have two different threads operating on different copies);

    不要试图使操作成为原子,并冒着两次加载数据的风险(并且可能有两个不同的线程在不同的副本上运行);

  • Serialize access to the cache, which means locking the entire cache just to load a single item;

    序列化对缓存的访问,这意味着锁定整个缓存只是为了加载单个项目;

  • Start reinventing the wheel just to get a few extra methods.

    开始重新发明*只是为了获得一些额外的方法。


Clarification: Example Timeline

澄清:示例时间表

Say that when an app starts, it needs to load 3 datasets which each take 10 seconds to load. Consider the following two timelines:

假设当应用程序启动时,它需要加载3个数据集,每个数据集需要10秒才能加载。请考虑以下两个时间表:

00:00 - Start loading Dataset 1
00:10 - Start loading Dataset 2
00:19 - User asks for Dataset 2

In the above case, if we don't use any kind of synchronization, the user has to wait a full 10 seconds for data that will be available in 1 second, because the code will see that the item is not yet loaded into the cache and try to reload it.

在上面的例子中,如果我们不使用任何类型的同步,用户必须等待整整10秒才能获得1秒内可用的数据,因为代码将看到该项目尚未加载到缓存中并尝试重新加载它。

00:00 - Start loading Dataset 1
00:10 - Start loading Dataset 2
00:11 - User asks for Dataset 1

In this case, the user is asking for data that's already in the cache. But if we serialize access to the cache, he'll have to wait another 9 seconds for no reason at all, because the cache manager (whatever that is) has no awareness of the specific item being asked for, only that "something" is being requested and "something" is in progress.

在这种情况下,用户要求已经在缓存中的数据。但是如果我们序列化对缓存的访问,他将无需等待另外9秒,因为缓存管理器(无论是什么)都不知道要求的特定项目,只有“某事”是被要求和“某事”正在进行中。


The Question:

问题:

Are there any caching libraries for .NET (pre-4.0) that do implement such atomic operations, as one might expect from a thread-safe cache?

.NET(4.0之前版本)是否存在实现此类原子操作的缓存库,正如人们对线程安全缓存所期望的那样?

Or, alternatively, is there some means to extend an existing "thread-safe" cache to support such operations, without serializing access to the cache (which would defeat the purpose of using a thread-safe implementation in the first place)? I doubt that there is, but maybe I'm just tired and ignoring an obvious workaround.

或者,是否有一些方法来扩展现有的“线程安全”缓存以支持此类操作,而无需序列化对缓存的访问(这将首先破坏使用线程安全实现的目的)?我怀疑是否有,但也许我只是累了,忽略了一个明显的解决方法。

Or... is there something else I'm missing? Is it just standard practice to let two competing threads steamroll each other if they happen to both be requesting the same item, at the same time, for the first time or after an expiration?

或者...还有什么我想念的吗?如果两个竞争线程碰巧同时第一次或在到期后同时请求相同的项目,那么让两个竞争线程相互流动只是标准做法吗?

4 个解决方案

#1


5  

I know your pain as I am one of the Architects of Dedoose. I have messed around with a lot of caching libraries and ended up building this one after much tribulation. The one assumption for this Cache Manager is that all collections stored by this class implement an interface to get a Guid as a "Id" property on each object. Being that this is for a RIA it includes a lot of methods for adding /updating /removing items from these collections.

我知道你的痛苦,因为我是Dedoose的建筑师之一。我已经搞砸了很多缓存库,最终在经历了多次灾难之后构建了这个库。此缓存管理器的一个假设是,此类存储的所有集合都实现了一个接口,以便将Guid作为每个对象的“Id”属性。因为这是一个RIA,它包含了很多方法来添加/更新/删除这些集合中的项目。

Here's my CollectionCacheManager

这是我的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;
}

Here is an example of how I use it:

以下是我如何使用它的示例:

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

Here's the Interface in question:

这是有问题的接口:

public interface IUniqueIdActiveRecord
{
    Guid Id { get; set; }

}

Hope this helps, I've been through hell and back a few times to finally arrive at this as the solution, and for us It's been a godsend, but I cannot guarantee that it's perfect, only that we haven't found an issue yet.

希望这会有所帮助,我已经经历过几次地狱并最终回到这里作为解决方案,对我们来说这是一个天赐之物,但我不能保证它是完美的,只是我们还没有找到问题。

#2


3  

It looks like the .NET 4.0 concurrent collections utilize new synchronization primitives that spin before switching context, in case a resource is freed quickly. So they're still locking, just in a more opportunistic way. If you think you data retrieval logic is shorter than the timeslice, then it seems like this would be highly beneficial. But you mentioned network, which makes me think this doesn't apply.

看起来.NET 4.0并发集合利用在切换上下文之前旋转的新同步原语,以防快速释放资源。所以他们仍然以更机会主义的方式锁定。如果您认为数据检索逻辑比时间片短,那么这似乎是非常有益的。但你提到网络,这让我觉得这不适用。

I would wait till you have a simple, synchronized solution in place, and measure the performance and behavior before assuming you will have performance issues related to concurrency.

我会等到你有一个简单的同步解决方案,并在假设你会遇到与并发相关的性能问题之前测量性能和行为。

If you're really concerned about cache contention, you can utilize an existing cache infrastructure and logically partition it into regions. Then synchronize access to each region independently.

如果您真的关心缓存争用,则可以利用现有缓存基础架构并将其逻辑分区到区域中。然后独立地同步对每个区域的访问。

An example strategy if your data set consists of items that are keyed on numeric IDs, and you want to partition your cache into 10 regions, you can (mod 10) the ID to determine which region they are in. You'd keep an array of 10 objects to lock on. All of the code can be written for a variable number of regions, which can be set via configuration, or determined at app start depending on the total number of items you predict/intend to cache.

一个示例策略,如果您的数据集包含键入数字ID的项目,并且您希望将缓存划分为10个区域,则可以(mod 10)ID来确定它们所在的区域。您将保留一个数组锁定的10个对象。所有代码都可以为可变数量的区域编写,可以通过配置设置,也可以在应用启动时确定,具体取决于您预测/打算缓存的项目总数。

If your cache hits are keyed in an abnormal way, you'll have to come up with some custom heuristic to partition the cache.

如果您的缓存命中以异常方式键入,则必须提供一些自定义启发式来对缓存进行分区。

Update (per comment): Well this has been fun. I think the following is about as fine-grained locking as you can hope for without going totally insane (or maintaining/synchronizing a dictionary of locks for each cache key). I haven't tested it so there are probably bugs, but the idea should be illustrated. Track a list of requested IDs, and then use that to decide if you need to get the item yourself, or if you merely need to wait for a previous request to finish. Waiting (and cache insertion) is synchronized with tightly-scoped thread blocking and signaling using Wait and PulseAll. Access to the requested ID list is synchronized with a tightly-scopedReaderWriterLockSlim.

更新(每条评论):这很有趣。我认为以下是关于细粒度锁定,你可以希望没有完全疯狂(或维护/同步每个缓存键的锁字典)。我没有测试它,所以可能存在错误,但应该说明这个想法。跟踪请求的ID列表,然后使用它来决定是否需要自己获取项目,或者只需要等待先前的请求完成。等待(和缓存插入)与使用Wait和PulseAll的严格范围的线程阻塞和信令同步。对请求的ID列表的访问与紧密的scoundReaderWriterLockSlim同步。

This is a read-only cache. If you doing creates/updates/deletes, you'll have to make sure you remove IDs from requestedIds once they're received (before the call to Monitor.PulseAll(_cache) you'll want to add another try..finally and acquire the _requestedIdsLock write-lock). Also, with creates/updates/deletes, the easiest way to manage the cache would be to merely remove the existing item from _cache if/when the underlying create/update/delete operation succeeds.

这是一个只读缓存。如果您正在创建/更新/删除,则必须确保在收到来自requestedIds之后删除ID(在调用Monitor.PulseAll(_cache)之前,您将要添加另一个try..finally并获取_requestedIdsLock写锁定。此外,通过创建/更新/删除,管理缓存的最简单方法是仅在基础创建/更新/删除操作成功时/从_cache中删除现有项目。

(Oops, see update 2 below.)

(糟糕,请参阅下面的更新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());
    }
}

Update 2:

更新2:

I misunderstood the behavior of UpgradeableReadLock... only one thread at a time can hold an UpgradeableReadLock. So the above should be refactored to only grab Read locks initially, and to completely relinquish them and acquire a full-fledged Write lock when adding items to _requestedIds.

我误解了UpgradeableReadLock的行为......一次只有一个线程可以容纳UpgradeableReadLock。因此,上面应该重构为最初只抓取Read锁,并在向_requestedIds添加项时完全放弃它们并获得完整的Write锁。

#3


1  

I implemented a simple library named MemoryCacheT. It's on GitHub and NuGet. It basically stores items in a ConcurrentDictionary and you can specify expiration strategy when adding items. Any feedback, review, suggestion is welcome.

我实现了一个名为MemoryCacheT的简单库。它在GitHub和NuGet上。它基本上将项目存储在ConcurrentDictionary中,您可以在添加项目时指定过期策略。欢迎任何反馈,评论和建议。

#4


0  

Finally came up with a workable solution to this, thanks to some dialogue in the comments. What I did was create a wrapper, which is a partially-implemented abstract base class that uses any standard cache library as the backing cache (just needs to implement the Contains, Get, Put, and Remove methods). At the moment I'm using the EntLib Caching Application Block for that, and it took a while to get this up and running because some aspects of that library are... well... not that well-thought-out.

最后提出了一个可行的解决方案,这要归功于评论中的一些对话。我所做的是创建一个包装器,它是一个部分实现的抽象基类,它使用任何标准缓存库作为后备缓存(只需要实现Contains,Get,Put和Remove方法)。目前我正在使用EntLib缓存应用程序块,并且需要一段时间才能启动并运行,因为该库的某些方面是......好吧......不是那么经过深思熟虑。

Anyway, the total code is now close to 1k lines so I'm not going to post the entire thing here, but the basic idea is:

无论如何,总代码现在接近1k行,所以我不打算在这里发布整个内容,但基本的想法是:

  1. Intercept all calls to the Get, Put/Add, and Remove methods.

    拦截对Get,Put / Add和Remove方法的所有调用。

  2. Instead of adding the original item, add an "entry" item which contains a ManualResetEvent in addition to a Value property. As per some advice given to me on an earlier question today, the entry implements a countdown latch, which is incremented whenever the entry is acquired and decremented whenever it is released. Both the loader and all future lookups participate in the countdown latch, so when the counter hits zero, the data is guaranteed to be available and the ManualResetEvent is destroyed in order to conserve resources.

    除了添加原始项之外,还添加一个“条目”项,除了Value属性外,还包含ManualResetEvent。根据今天在早期问题上给我的一些建议,该条目实现了一个倒计时锁存器,每当获取该条目时它就递增,并且每当它被释放时递减。加载器和所有将来的查找都参与倒计时锁存器,因此当计数器达到零时,保证数据可用并且销毁ManualResetEvent以节省资源。

  3. When an entry has to be lazy-loaded, the entry is created and added to the backing cache right away, with the event in an unsignaled state. Subsequent calls to either the new GetOrAdd method or the intercepted Get methods will find this entry, and either wait on the event (if the event exists) or return the associated value immediately (if the event does not exist).

    当条目必须延迟加载时,将创建条目并立即将其添加到后备缓存中,事件处于未签名状态。对新GetOrAdd方法或截获的Get方法的后续调用将找到此条目,并等待事件(如果事件存在)或立即返回关联值(如果事件不存在)。

  4. The Put method adds an entry with no event; these look the same as entries for which lazy-loading has already been completed.

    Put方法添加一个没有事件的条目;这些看起来与已经完成延迟加载的条目相同。

  5. Because the GetOrAdd still implements a Get followed by an optional Put, this method is synchronized (serialized) against the Put and Remove methods, but only to add the incomplete entry, not for the entire duration of the lazy load. The Get methods are not serialized; effectively the entire interface works like an automatic reader-writer lock.

    因为GetOrAdd仍然实现了Get后跟可选的Put,所以此方法与Put和Remove方法同步(序列化),但仅用于添加不完整的条目,而不是在延迟加载的整个持续时间内。 Get方法没有序列化;有效地,整个界面就像一个自动读写器锁。

It's still a work in progress, but I've run it through a dozen unit tests and it seems to be holding up. It behaves correctly for both the scenarios described in the question. In other words:

它仍然是一项正在进行的工作,但我已经通过十几个单元测试来运行它,它似乎正在坚持下去。它对于问题中描述的两种情况都表现正确。换一种说法:

  • A call to long-running lazy-load (GetOrAdd) for key X (simulated by Thread.Sleep) which takes 10 seconds, followed by another GetOrAdd for the same key X on a different thread exactly 9 seconds later, results in both threads receiving the correct data at the same time (10 seconds from T0). Loads are not duplicated.

    对于密钥X(由Thread.Sleep进行模拟)的长时间运行延迟加载(GetOrAdd)调用需要10秒,然后在9秒后对另一个线程上的相同密钥X执行另一个GetOrAdd,导致两个线程都接收到同时输入正确的数据(距离T0 10秒)。载荷不重复。

  • Immediately loading a value for key X, then starting a long-running lazy-load for key Y, then requesting key X on another thread (before Y is finished), immediately gives back the value for X. Blocking calls are isolated to the relevant key.

    立即加载密钥X的值,然后为密钥Y启动长时间运行的延迟加载,然后在另一个线程上请求密钥X(在Y完成之前),立即返回X的值。阻塞调用被隔离到相关的键。

It also gives what I think is the most intuitive result for when you begin a lazy-load and then immediately remove the key from the cache; the thread that originally requested the value will get the real value, but any other threads that request the same key at any time after the removal will get nothing back (null) and return immediately.

它还提供了我认为当你开始延迟加载然后立即从缓存中删除密钥时最直观的结果;最初请求该值的线程将获得实际值,但在删除后随时请求相同键的任何其他线程将不会返回任何内容(null)并立即返回。

All in all I'm pretty happy with it. I still wish there was a library that did this for me, but I suppose, if you want something done right... well, you know.

总而言之,我对它很满意。我仍然希望有一个图书馆为我做了这个,但我想,如果你想做正确的事......好吧,你知道。

#1


5  

I know your pain as I am one of the Architects of Dedoose. I have messed around with a lot of caching libraries and ended up building this one after much tribulation. The one assumption for this Cache Manager is that all collections stored by this class implement an interface to get a Guid as a "Id" property on each object. Being that this is for a RIA it includes a lot of methods for adding /updating /removing items from these collections.

我知道你的痛苦,因为我是Dedoose的建筑师之一。我已经搞砸了很多缓存库,最终在经历了多次灾难之后构建了这个库。此缓存管理器的一个假设是,此类存储的所有集合都实现了一个接口,以便将Guid作为每个对象的“Id”属性。因为这是一个RIA,它包含了很多方法来添加/更新/删除这些集合中的项目。

Here's my CollectionCacheManager

这是我的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;
}

Here is an example of how I use it:

以下是我如何使用它的示例:

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

Here's the Interface in question:

这是有问题的接口:

public interface IUniqueIdActiveRecord
{
    Guid Id { get; set; }

}

Hope this helps, I've been through hell and back a few times to finally arrive at this as the solution, and for us It's been a godsend, but I cannot guarantee that it's perfect, only that we haven't found an issue yet.

希望这会有所帮助,我已经经历过几次地狱并最终回到这里作为解决方案,对我们来说这是一个天赐之物,但我不能保证它是完美的,只是我们还没有找到问题。

#2


3  

It looks like the .NET 4.0 concurrent collections utilize new synchronization primitives that spin before switching context, in case a resource is freed quickly. So they're still locking, just in a more opportunistic way. If you think you data retrieval logic is shorter than the timeslice, then it seems like this would be highly beneficial. But you mentioned network, which makes me think this doesn't apply.

看起来.NET 4.0并发集合利用在切换上下文之前旋转的新同步原语,以防快速释放资源。所以他们仍然以更机会主义的方式锁定。如果您认为数据检索逻辑比时间片短,那么这似乎是非常有益的。但你提到网络,这让我觉得这不适用。

I would wait till you have a simple, synchronized solution in place, and measure the performance and behavior before assuming you will have performance issues related to concurrency.

我会等到你有一个简单的同步解决方案,并在假设你会遇到与并发相关的性能问题之前测量性能和行为。

If you're really concerned about cache contention, you can utilize an existing cache infrastructure and logically partition it into regions. Then synchronize access to each region independently.

如果您真的关心缓存争用,则可以利用现有缓存基础架构并将其逻辑分区到区域中。然后独立地同步对每个区域的访问。

An example strategy if your data set consists of items that are keyed on numeric IDs, and you want to partition your cache into 10 regions, you can (mod 10) the ID to determine which region they are in. You'd keep an array of 10 objects to lock on. All of the code can be written for a variable number of regions, which can be set via configuration, or determined at app start depending on the total number of items you predict/intend to cache.

一个示例策略,如果您的数据集包含键入数字ID的项目,并且您希望将缓存划分为10个区域,则可以(mod 10)ID来确定它们所在的区域。您将保留一个数组锁定的10个对象。所有代码都可以为可变数量的区域编写,可以通过配置设置,也可以在应用启动时确定,具体取决于您预测/打算缓存的项目总数。

If your cache hits are keyed in an abnormal way, you'll have to come up with some custom heuristic to partition the cache.

如果您的缓存命中以异常方式键入,则必须提供一些自定义启发式来对缓存进行分区。

Update (per comment): Well this has been fun. I think the following is about as fine-grained locking as you can hope for without going totally insane (or maintaining/synchronizing a dictionary of locks for each cache key). I haven't tested it so there are probably bugs, but the idea should be illustrated. Track a list of requested IDs, and then use that to decide if you need to get the item yourself, or if you merely need to wait for a previous request to finish. Waiting (and cache insertion) is synchronized with tightly-scoped thread blocking and signaling using Wait and PulseAll. Access to the requested ID list is synchronized with a tightly-scopedReaderWriterLockSlim.

更新(每条评论):这很有趣。我认为以下是关于细粒度锁定,你可以希望没有完全疯狂(或维护/同步每个缓存键的锁字典)。我没有测试它,所以可能存在错误,但应该说明这个想法。跟踪请求的ID列表,然后使用它来决定是否需要自己获取项目,或者只需要等待先前的请求完成。等待(和缓存插入)与使用Wait和PulseAll的严格范围的线程阻塞和信令同步。对请求的ID列表的访问与紧密的scoundReaderWriterLockSlim同步。

This is a read-only cache. If you doing creates/updates/deletes, you'll have to make sure you remove IDs from requestedIds once they're received (before the call to Monitor.PulseAll(_cache) you'll want to add another try..finally and acquire the _requestedIdsLock write-lock). Also, with creates/updates/deletes, the easiest way to manage the cache would be to merely remove the existing item from _cache if/when the underlying create/update/delete operation succeeds.

这是一个只读缓存。如果您正在创建/更新/删除,则必须确保在收到来自requestedIds之后删除ID(在调用Monitor.PulseAll(_cache)之前,您将要添加另一个try..finally并获取_requestedIdsLock写锁定。此外,通过创建/更新/删除,管理缓存的最简单方法是仅在基础创建/更新/删除操作成功时/从_cache中删除现有项目。

(Oops, see update 2 below.)

(糟糕,请参阅下面的更新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());
    }
}

Update 2:

更新2:

I misunderstood the behavior of UpgradeableReadLock... only one thread at a time can hold an UpgradeableReadLock. So the above should be refactored to only grab Read locks initially, and to completely relinquish them and acquire a full-fledged Write lock when adding items to _requestedIds.

我误解了UpgradeableReadLock的行为......一次只有一个线程可以容纳UpgradeableReadLock。因此,上面应该重构为最初只抓取Read锁,并在向_requestedIds添加项时完全放弃它们并获得完整的Write锁。

#3


1  

I implemented a simple library named MemoryCacheT. It's on GitHub and NuGet. It basically stores items in a ConcurrentDictionary and you can specify expiration strategy when adding items. Any feedback, review, suggestion is welcome.

我实现了一个名为MemoryCacheT的简单库。它在GitHub和NuGet上。它基本上将项目存储在ConcurrentDictionary中,您可以在添加项目时指定过期策略。欢迎任何反馈,评论和建议。

#4


0  

Finally came up with a workable solution to this, thanks to some dialogue in the comments. What I did was create a wrapper, which is a partially-implemented abstract base class that uses any standard cache library as the backing cache (just needs to implement the Contains, Get, Put, and Remove methods). At the moment I'm using the EntLib Caching Application Block for that, and it took a while to get this up and running because some aspects of that library are... well... not that well-thought-out.

最后提出了一个可行的解决方案,这要归功于评论中的一些对话。我所做的是创建一个包装器,它是一个部分实现的抽象基类,它使用任何标准缓存库作为后备缓存(只需要实现Contains,Get,Put和Remove方法)。目前我正在使用EntLib缓存应用程序块,并且需要一段时间才能启动并运行,因为该库的某些方面是......好吧......不是那么经过深思熟虑。

Anyway, the total code is now close to 1k lines so I'm not going to post the entire thing here, but the basic idea is:

无论如何,总代码现在接近1k行,所以我不打算在这里发布整个内容,但基本的想法是:

  1. Intercept all calls to the Get, Put/Add, and Remove methods.

    拦截对Get,Put / Add和Remove方法的所有调用。

  2. Instead of adding the original item, add an "entry" item which contains a ManualResetEvent in addition to a Value property. As per some advice given to me on an earlier question today, the entry implements a countdown latch, which is incremented whenever the entry is acquired and decremented whenever it is released. Both the loader and all future lookups participate in the countdown latch, so when the counter hits zero, the data is guaranteed to be available and the ManualResetEvent is destroyed in order to conserve resources.

    除了添加原始项之外,还添加一个“条目”项,除了Value属性外,还包含ManualResetEvent。根据今天在早期问题上给我的一些建议,该条目实现了一个倒计时锁存器,每当获取该条目时它就递增,并且每当它被释放时递减。加载器和所有将来的查找都参与倒计时锁存器,因此当计数器达到零时,保证数据可用并且销毁ManualResetEvent以节省资源。

  3. When an entry has to be lazy-loaded, the entry is created and added to the backing cache right away, with the event in an unsignaled state. Subsequent calls to either the new GetOrAdd method or the intercepted Get methods will find this entry, and either wait on the event (if the event exists) or return the associated value immediately (if the event does not exist).

    当条目必须延迟加载时,将创建条目并立即将其添加到后备缓存中,事件处于未签名状态。对新GetOrAdd方法或截获的Get方法的后续调用将找到此条目,并等待事件(如果事件存在)或立即返回关联值(如果事件不存在)。

  4. The Put method adds an entry with no event; these look the same as entries for which lazy-loading has already been completed.

    Put方法添加一个没有事件的条目;这些看起来与已经完成延迟加载的条目相同。

  5. Because the GetOrAdd still implements a Get followed by an optional Put, this method is synchronized (serialized) against the Put and Remove methods, but only to add the incomplete entry, not for the entire duration of the lazy load. The Get methods are not serialized; effectively the entire interface works like an automatic reader-writer lock.

    因为GetOrAdd仍然实现了Get后跟可选的Put,所以此方法与Put和Remove方法同步(序列化),但仅用于添加不完整的条目,而不是在延迟加载的整个持续时间内。 Get方法没有序列化;有效地,整个界面就像一个自动读写器锁。

It's still a work in progress, but I've run it through a dozen unit tests and it seems to be holding up. It behaves correctly for both the scenarios described in the question. In other words:

它仍然是一项正在进行的工作,但我已经通过十几个单元测试来运行它,它似乎正在坚持下去。它对于问题中描述的两种情况都表现正确。换一种说法:

  • A call to long-running lazy-load (GetOrAdd) for key X (simulated by Thread.Sleep) which takes 10 seconds, followed by another GetOrAdd for the same key X on a different thread exactly 9 seconds later, results in both threads receiving the correct data at the same time (10 seconds from T0). Loads are not duplicated.

    对于密钥X(由Thread.Sleep进行模拟)的长时间运行延迟加载(GetOrAdd)调用需要10秒,然后在9秒后对另一个线程上的相同密钥X执行另一个GetOrAdd,导致两个线程都接收到同时输入正确的数据(距离T0 10秒)。载荷不重复。

  • Immediately loading a value for key X, then starting a long-running lazy-load for key Y, then requesting key X on another thread (before Y is finished), immediately gives back the value for X. Blocking calls are isolated to the relevant key.

    立即加载密钥X的值,然后为密钥Y启动长时间运行的延迟加载,然后在另一个线程上请求密钥X(在Y完成之前),立即返回X的值。阻塞调用被隔离到相关的键。

It also gives what I think is the most intuitive result for when you begin a lazy-load and then immediately remove the key from the cache; the thread that originally requested the value will get the real value, but any other threads that request the same key at any time after the removal will get nothing back (null) and return immediately.

它还提供了我认为当你开始延迟加载然后立即从缓存中删除密钥时最直观的结果;最初请求该值的线程将获得实际值,但在删除后随时请求相同键的任何其他线程将不会返回任何内容(null)并立即返回。

All in all I'm pretty happy with it. I still wish there was a library that did this for me, but I suppose, if you want something done right... well, you know.

总而言之,我对它很满意。我仍然希望有一个图书馆为我做了这个,但我想,如果你想做正确的事......好吧,你知道。