【问题标题】:Thread-safe cache libraries for .NET.NET 的线程安全缓存库
【发布时间】:2012-09-24 03:48:10
【问题描述】:

背景:

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

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

我认为这种方法是用于缓存/查找功能的原语,显然框架设计者也意识到了这一点——这就是这些方法存在于并发集合中的原因。然而,除了我还没有在生产应用程序中使用 .NET 4.0 之外,字典并不是一个成熟的缓存——它没有过期、持久/分布式存储等功能。


为什么这很重要:

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

所以我似乎还有一些同样糟糕的选择:

  • 根本不要尝试使操作原子化,否则会冒数据被加载两次的风险(并且可能有两个不同的线程在不同的副本上运行);

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

  • 重新发明轮子只是为了获得一些额外的方法。


澄清:时间线示例

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

00:00 - 开始加载数据集 1 00:10 - 开始加载数据集 2 00:19 - 用户请求数据集 2

在上述情况下,如果我们不使用任何类型的同步,用户必须等待整整 10 秒才能获得 1 秒后可用的数据,因为代码会看到该项目尚未加载进入缓存并尝试重新加载它。

00:00 - 开始加载数据集 1 00:10 - 开始加载数据集 2 00:11 - 用户请求数据集 1

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


问题:

是否有任何用于 .NET(4.0 之前)的缓存库确实实现了此类原子操作,正如人们可能期望从线程安全缓存中获得的那样?

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

或者...我还缺少其他东西吗?如果两个竞争线程碰巧在第一次或过期后同时请求同一个项目,这只是标准做法吗?

【问题讨论】:

  • 我很好奇你所说的不序列化对缓存的访问是什么意思。一种或另一种方式,必须序列化对相同资源的访问(无论是在初始创建期间还是到期后)。应该可以通过键而不是整个缓存来序列化访问,但在某些时候,必须进行一些序列化......除非我自己错过了什么......
  • @jrista:序列化不是线程安全的唯一手段;还有读写器锁等。更重要的是,线程安全库应该自己处理所有这些逻辑。我见过的所有缓存实现都只是“线程安全”,即“多个线程同时调用操作不会破坏缓存”,但我想要的是“对常见多步原子操作的内置支持,例如缓存项的延迟加载”。
  • 好吧,即使是在读写锁的情况下……如果只有一个写入器,所有其他写入器和所有读取器都会被阻塞,直到单个写入器释放。除了最基本的操作(例如可通过 Interlocked 类访问的递增/递减、交换等)之外,序列化通常是线程同步的不可避免的结果。你可以把它埋起来,想藏多少就藏多少,但在某些时候,它会发生。如果你想要一个线程安全的缓存来做你正在寻找的事情,这是可能的,但你可能不得不自己写......
  • ...我认为您正在寻找的关键是找到一种方法来序列化尽可能少的线程。这是可行的,尽管它可能不是微不足道的。您不一定需要锁定整个缓存并序列化尝试使用它的每个线程......您只需要找到一种聪明的方法来仅同步尝试访问缓存中相同内容的线程(或尽可能少的线程。)您可以通过几种方式来实现这一点,例如分区、可锁定和共享查找键(确实很复杂,但如果你弄清楚了,它是最好的颗粒)等。
  • @jrista:如果我必须自己写,那就这样吧,我希望有其他选择。我不认为期望缓存能够序列化对单个项目的访问而不是整个字典是不合理的,希望缓存能够说“嘿,再等几秒钟,它已经在路上了”,而不仅仅是“是的,我有它”或“不,我没有它。”

标签: .net multithreading caching atomic


【解决方案1】:

我知道你的痛苦,因为我是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; }

}

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

【讨论】:

  • 这确实看起来解决了手头的问题,尽管(在我的高度偏见看来)不像我想出的那样光滑。 ;) 绝对是 +1 的实现,我想我也会将接受的答案更改为这个答案,因为它处理了对问题至关重要的延迟加载。
  • 大声笑,是的,在此之前我有一些更聪明的,但在调试并发问题时遇到了一些疯狂的问题,最后以我可能的最防弹的方式再次重建了吸盘。希望对您有所帮助!
【解决方案2】:

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

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

如果您真的担心缓存争用,您可以利用现有的缓存基础架构并将其逻辑划分为多个区域。然后分别同步对每个区域的访问。

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

如果您的缓存命中是以异常方式键入的,您将不得不提出一些自定义启发式方法来对缓存进行分区。

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

这是一个只读缓存。如果您执行创建/更新/删除,则必须确保在收到 ID 后从 requestedIds 中删除(在调用 Monitor.PulseAll(_cache) 之前,您需要添加另一个 try..finally 并获取 @ 987654327@ 写锁)。此外,使用创建/更新/删除,管理缓存的最简单方法是仅在底层创建/更新/删除操作成功时从 _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添加项目时完全放弃它们并获取一个完整的写锁。

【讨论】:

  • 谢谢,这与我的想法非常接近;我问这个问题的原因是实现我自己的缓存还需要我实现过期、清除、持久性、流序列化以及您在生产级缓存实现中会发现的所有其他数千行代码,即我想避免的。我肯定会给你+1的实施;尽管如此,更大的问题仍未解决。
  • 我没有最终使用这个实现,但你有最好的答案,所以你明白了!
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    多亏了 cmets 中的一些对话,终于想出了一个可行的解决方案。我所做的是创建一个包装器,它是一个部分实现的抽象基类,它使用任何标准缓存库作为后备缓存(只需要实现Contains、Get、Put 和Remove 方法)。目前我正在为此使用 EntLib 缓存应用程序块,它需要一段时间才能启动并运行,因为该库的某些方面......嗯......没有经过深思熟虑。

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

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

    2. 除了添加一个 Value 属性之外,添加一个包含 ManualResetEvent 的“条目”项,而不是添加原始项。根据今天早些时候对我提出的一些建议,该条目实现了一个倒计时锁存器,每当获取该条目时该锁存器就会递增,而每当它释放时该锁存器就会递减。加载程序和所有未来的查找都参与倒计时锁存器,因此当计数器达到零时,保证数据可用并且ManualResetEvent 被销毁以节省资源。

    3. 当必须延迟加载条目时,会立即创建该条目并将其添加到后备缓存中,并且该事件处于未发出信号的状态。对新的GetOrAdd 方法或拦截的Get 方法的后续调用将找到此条目,并等待事件(如果事件存在)或立即返回相关值(如果事件不存在)。

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

    5. 因为GetOrAdd 仍然实现Get 后跟可选的Put,所以此方法与Put 和Remove 方法同步(序列化),但仅 em> 添加不完整的条目,而不是延迟加载的整个持续时间。 Get 方法未序列化;实际上,整个界面就像一个自动读写锁一样工作。

    这项工作仍在进行中,但我已经通过十几个单元测试运行了它,它似乎一直在坚持。对于问题中描述的两种情况,它的行为都是正确的。换句话说:

    • 为密钥 X(由 Thread.Sleep 模拟)调用长时间运行的延迟加载 (GetOrAdd),这需要 10 秒,然后是另一个 GetOrAdd同一键 X 恰好在 9 秒后在不同的线程上,导致两个线程同时接收正确的数据(距 T0 10 秒)。负载不重复。

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

    当您开始延迟加载然后立即从缓存中删除密钥时,它还给出了我认为最直观的结果;最初请求该值的线程将获得真正的值,但在删除后的任何时间请求相同密钥的任何其他线程都将一无所获(null)并立即返回。

    总的来说,我很满意。我仍然希望有一个图书馆可以为我做这件事,但我想,如果你想把事情做好……好吧,你知道的。

    【讨论】:

    • 酷,如果你能找到时间/精力/资源把它贴在某个地方,那就太好了!
    • @gWiz:人们通常在哪里托管这些类型的东西?我通常不做整个开源的事情,尽管这实际上已经成为一个非常有用的库。真正的魔力在于方法拦截库,我用它来装饰带有CacheAttribute 的方法,并让它根据参数自动分配缓存槽。我很高兴把它放在某个地方,我只是没有源代码分发的经验。 :P
    • 这是个好问题。如果您想将其设置为拥有完全托管的项目基础架构,那么肯定有 codeplex.com 或 sourceforge.net。我相信两者都使用 svn 并且需要一些工作来设置。但是,如果您只是想将文件托管在某个地方以供参考(并且如果您没有博客),您只需将其上传到 Google 文档(并将共享权限更改为不需要用户登录即可查看)。
    • @Aaronnaught:你最后把这个上传到什么地方了吗?我觉得这很有趣。
    • @Aaronnaught 如果您能分享您的解决方案,那就太好了
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-02-10
    • 2012-07-11
    • 1970-01-01
    • 2017-10-15
    • 2011-03-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多