Потокобезопасные библиотеки кеша для .NET - PullRequest
31 голосов
/ 25 февраля 2010

Справочная информация:

Я поддерживаю несколько приложений Winforms и библиотек классов, которые могут или уже могут извлечь выгоду из кэширования. Мне также известно о блоке приложения для кэширования и пространстве имен System.Web.Caching (которое, насколько я знаю, вполне подходит для использования вне ASP.NET). .

Я обнаружил, что, хотя оба вышеперечисленных класса являются технически «поточно-ориентированными» в том смысле, что отдельные методы синхронизированы, на самом деле они не очень хорошо разработаны для многопоточных сценариев. В частности, они не реализуют GetOrAdd метод , подобный методу в новом классе ConcurrentDictionary в .NET 4.0.

Я считаю такой метод примитивом для функций кэширования / поиска, и, очевидно, разработчики Framework это тоже поняли - поэтому методы существуют в параллельных коллекциях. Однако, несмотря на то, что я пока не использую .NET 4.0 в производственных приложениях, словарь не является полноценным кешем - он не имеет таких функций, как истечение срока хранения, постоянное / распределенное хранилище и т. Д.


Почему это важно:

Довольно типичный дизайн в приложении «богатого клиента» (или даже в некоторых веб-приложениях) состоит в том, чтобы начать предварительную загрузку кэша сразу после запуска приложения, блокируя, если клиент запрашивает данные, которые еще не загружены (впоследствии кэшируется это для будущего использования). Если пользователь быстро просматривает свой рабочий процесс или медленное сетевое соединение, для клиента нет ничего необычного в том, чтобы конкурировать с предзагрузчиком, и на самом деле не имеет большого смысла запрашивать одни и те же данные дважды особенно если запрос относительно дорогой.

Так что мне кажется, что у меня осталось несколько одинаково паршивых вариантов:

  • Ни в коем случае не пытайтесь сделать операцию атомарной, и рискуйте загрузкой данных дважды (и, возможно, двумя разными потоками, работающими с разными копиями);

  • Сериализация доступа к кешу, что означает блокировку всего кеша только для загрузки отдельного элемента ;

  • Начните изобретать велосипед, чтобы получить несколько дополнительных методов.


Уточнение: Пример временной шкалы

Скажите, что при запуске приложения ему нужно загрузить 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), в которых do реализует такие атомарные операции, как можно ожидать от поточно-безопасного кеша?

Или, в качестве альтернативы, существуют ли какие-либо средства для расширения существующего "поточно-ориентированного" кэша для поддержки таких операций, без сериализации доступа к кешу (что противоречило бы цели использования поточно-безопасного реализация в первую очередь)? Я сомневаюсь, что есть, но, возможно, я просто устал и игнорирую очевидный обходной путь.

Или ... что-то еще мне не хватает? Является ли обычной практикой позволять двум конкурирующим потокам обкатывать друг друга, если они оба запрашивают один и тот же элемент в одно и то же время в первый раз или после истечения срока действия?

Ответы [ 4 ]

6 голосов
/ 08 декабря 2010

Я знаю вашу боль, поскольку я один из архитекторов Дедуза . Я возился с большим количеством библиотек кеширования и закончил создание этой библиотеки после долгих испытаний. Одно из предположений для этого диспетчера кэша состоит в том, что все коллекции, хранящиеся в этом классе, реализуют интерфейс для получения 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; }

}

Надеюсь, это поможет, я несколько раз проходил через ад и обратно, чтобы наконец прийти к этому как к решению, и для нас это было находкой, но я не могу гарантировать, что оно идеально, только то, что мы не нашли проблема еще.

3 голосов
/ 25 февраля 2010

Похоже, что в параллельных коллекциях .NET 4.0 используются новые примитивы синхронизации, которые вращаются перед переключением контекста в случае быстрого освобождения ресурса.Таким образом, они все еще запираются, просто более гибким способом.Если вы думаете, что логика поиска данных короче, чем временной интервал, то кажется, что это будет очень полезно.Но вы упомянули сеть, которая заставляет меня думать, что это неприменимо.

Я бы подождал, пока у вас есть простое синхронизированное решение, и измерил бы производительность и поведение, прежде чем предполагать, что у вас будут проблемы с производительностью, связанные спараллелизм.

Если вы действительно обеспокоены конфликтами в кеше, вы можете использовать существующую инфраструктуру кеша и логически разбить ее на регионы.Затем синхронизируйте доступ к каждому региону независимо.

Пример стратегии, если ваш набор данных состоит из элементов, которые имеют цифровые идентификаторы, и вы хотите разделить кэш на 10 регионов, вы можете (мод 10) идентифицироватьчтобы определить, в каком регионе они находятся. Вы должны сохранить массив из 10 объектов для блокировки.Весь код может быть написан для переменного числа регионов, который может быть установлен через конфигурацию или определен при запуске приложения в зависимости от общего количества элементов, которые вы прогнозируете / намереваетесь кешировать.

Если ваш кэш попадетнеправильно настроены, вам придется придумать какую-то собственную эвристику для разделения кэша.

Обновление (за комментарий): Ну, это было весело.Я думаю, что следующее - это настолько тонкая блокировка, на которую можно надеяться, не сходя с ума (или поддерживая / синхронизируя словарь блокировок для каждого ключа кэша).Я не проверял это, поэтому, возможно, есть ошибки, но идея должна быть проиллюстрирована.Отследите список запрошенных идентификаторов, а затем используйте его, чтобы решить, нужно ли вам получать товар самостоятельно или вам просто нужно дождаться завершения предыдущего запроса.Ожидание (и вставка в кэш) синхронизируется с блокировкой потоков с ограниченным объемом и сигнализацией с использованием Wait и PulseAll.Доступ к запрошенному списку идентификаторов синхронизируется с жестко ограниченным ReaderWriterLockSlim.

Это кэш только для чтения.Если вы делаете создание / обновление / удаление, вы должны будете убедиться, что удалили идентификаторы из 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.

2 голосов
/ 23 сентября 2012

Я реализовал простую библиотеку с именем MemoryCacheT. Это на GitHub и NuGet . Он в основном хранит элементы в ConcurrentDictionary, и вы можете указать стратегию истечения срока действия при добавлении элементов. Любые отзывы, отзывы, предложения приветствуются.

0 голосов
/ 26 февраля 2010

Наконец-то нашли подходящее решение, благодаря некоторому диалогу в комментариях. Я создал оболочку, представляющую собой частично реализованный абстрактный базовый класс, который использует любую стандартную библиотеку кэша в качестве резервного кэша (просто необходимо реализовать методы Contains, Get, Put и Remove ). В настоящее время я использую для этого блок кэширования приложений EntLib, и потребовалось некоторое время, чтобы его запустить и запустить, потому что некоторые аспекты этой библиотеки ... ну ... не так хорошо продуманы.

В любом случае, общий код теперь близок к 1 тыс. Строк, поэтому я не собираюсь публиковать здесь все, но основная идея:

  1. Перехватывать все вызовы методов Get, Put/Add и Remove.

  2. Вместо добавления исходного элемента добавьте элемент «entry», который содержит ManualResetEvent в дополнение к свойству Value. Согласно некоторым советам, данным мне по более раннему вопросу сегодня, в записи реализована защелка обратного отсчета, которая увеличивается при каждом получении записи и уменьшается при каждой публикации. И загрузчик, и все последующие поиски участвуют в защелке обратного отсчета, поэтому, когда счетчик достигает нуля, данные гарантированно становятся доступными, а ManualResetEvent уничтожается для сохранения ресурсов.

  3. Когда запись должна быть загружена с отложенной загрузкой, запись сразу создается и добавляется в резервный кэш с событием в неподписанном состоянии. Последующие вызовы либо нового метода GetOrAdd, либо перехваченных методов Get найдут эту запись и либо ждут события (если событие существует), либо немедленно возвращают соответствующее значение (если событие не существует).

  4. Метод Put добавляет запись без события; они выглядят так же, как записи, для которых отложенная загрузка уже завершена.

  5. Поскольку GetOrAdd все еще реализует Get, за которым следует необязательный Put, этот метод синхронизируется (сериализуется) с методами Put и Remove, но только добавить незавершенную запись, а не на весь период ленивой загрузки. Методы Get сериализуются , а не ; фактически весь интерфейс работает как автоматическая блокировка чтения-записи.

Это все еще в стадии разработки, но я прошел через дюжину юнит-тестов, и похоже, что оно не работает. Это ведет себя правильно для обоих сценариев, описанных в вопросе. Другими словами:

  • Вызов длительной ленивой нагрузки (GetOrAdd) для клавиши X (имитируется Thread.Sleep), который занимает 10 секунд, после чего следует еще один GetOrAdd для того же клавиша X в другом потоке ровно через 9 секунд приводит к тому, что оба потока получают правильные данные одновременно (10 секунд из T 0 ). Нагрузки не дублируются.

  • Немедленная загрузка значения для ключа X , затем запуск длительной отложенной загрузки для ключа Y , затем запрос ключа X on другой поток (до окончания Y ) немедленно возвращает значение для X . Блокирующие вызовы изолированы для соответствующего ключа.

Это также дает, как мне кажется, наиболее интуитивный результат, когда вы начинаете ленивую загрузку, а затем немедленно удаляете ключ из кеша; поток, который первоначально запросил значение, получит реальное значение, но любые другие потоки, которые запрашивают тот же ключ в любое время после удаления, ничего не получат обратно (null) и вернутся немедленно.

В целом, я очень доволен этим. Я все еще хотел бы, чтобы была библиотека, которая сделала это для меня, но я полагаю, если вы хотите, чтобы что-то было сделано правильно ... ну, вы знаете.

...