Подтвердить что ты не робот

Резервные кеш-библиотеки для .NET.

Фон:

Я поддерживаю несколько приложений Winforms и библиотеки классов, которые могут или уже могут извлечь выгоду из кеширования. Я также знаю блок кэширования приложений и System.Web.Caching namespace (которое, из того, что я собрал, отлично подходит для использования вне 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), которые делают реализуют такие атомные операции, как можно было бы ожидать из поточно-безопасного кеша?

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

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

4b9b3361

Ответ 1

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

}

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

Ответ 2

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

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

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

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

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

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

Это кеш-доступ только для чтения. Если вы создаете/обновляете/удаляете, вам нужно будет убедиться, что вы удаляете идентификаторы из requestedIds после их получения (перед вызовом Monitor.PulseAll(_cache) вам нужно добавить еще try..finally и получить _requestedIdsLock write-lock). Кроме того, при создании/обновлении/удалении самым простым способом управления кешем было бы просто удалить существующий элемент из _cache, если/когда базовая операция create/update/delete завершается успешно.

(Ой, см. обновление 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.

Ответ 3

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

Ответ 4

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

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

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

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

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

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

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

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

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

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

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

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