Thread-bezpieczne biblioteki pamięci podręcznej for.NET

Background:

Utrzymuję kilka aplikacji Winforms i bibliotek klas, które mogą lub już korzystają z buforowania. Jestem również świadomy bloku aplikacji buforowania i systemu .Www.Buforowanie przestrzeni nazw (która, z tego co zebrałem, jest całkowicie w porządku, aby używać Na Zewnątrz ASP.NET).

Odkryłem, że chociaż obie powyższe klasy są technicznie "bezpieczne dla wątków" w tym sensie, że poszczególne metody są zsynchronizowane, tak naprawdę nie wydaje się być zaprojektowany szczególnie dobrze dla scenariuszy wielowątkowych. W szczególności nie realizują GetOrAdd metoda podobna do tej w nowej klasie ConcurrentDictionary W. NET 4.0.

Uważam taką metodę za prymitywną dla funkcji buforowania/wyszukiwania, i oczywiście projektanci frameworku również to sobie uświadomili - dlatego metody istnieją w współbieżnych kolekcjach. Jednak pomijając fakt, że nie używam. NET 4.0 jeszcze w aplikacjach produkcyjnych, słownik nie jest pełnowartościową pamięcią podręczną - nie ma funkcji takich jak wygasanie, trwała/rozproszona pamięć masowa itp.


Dlaczego jest to ważne:

[4]} dość typowym projektem w aplikacji "rich client" (lub nawet niektórych aplikacjach internetowych) jest rozpoczęcie wstępnego ładowania pamięci podręcznej zaraz po uruchomieniu aplikacji, blokowanie, jeśli klient żąda danych, które nie zostały jeszcze załadowane (następnie buforowanie go do wykorzystania w przyszłości). Jeśli użytkownik szybko przechodzi przez swój przepływ pracy lub jeśli połączenie sieciowe jest wolne, nie jest niczym niezwykłym, aby Klient konkurował z preloaderem i naprawdę nie ma sensu prosić dwa razy o te same dane, zwłaszcza jeśli żądanie jest stosunkowo drogie.

Więc wydaje mi się, że zostało mi kilka równie kiepskich opcji:

  • Nie próbuj uczynić operacji atomową w ogóle i ryzykuj, że dane zostaną załadowane dwa razy (i prawdopodobnie będą miały dwa różne wątki działające na różnych kopiach);

  • Serialize dostęp do cache, czyli blokowanie całego cache tylko po to, aby załadować pojedynczy element;

  • Zacznij wymyślać koło na nowo, aby uzyskać kilka dodatkowych metod.


Wyjaśnienie: Przykład Timeline

Powiedzmy, że gdy aplikacja się uruchamia, musi załadować 3 zestawy danych, z których każdy zajmuje 10 sekund. Rozważmy następujące dwie linie czasowe:

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

W powyższym przypadku, jeśli nie używamy żadnej synchronizacji, użytkownik musi poczekać na pełne 10 sekund dla danych, które będą dostępne w ciągu 1 sekundy, ponieważ kod zobaczy, że element nie jest jeszcze załadowany do pamięci podręcznej i spróbuje go przeładować.

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

W tym przypadku użytkownik prosi o dane, które są już w buforze. Ale jeśli serializujemy dostęp do pamięci podręcznej, będzie musiał odczekać kolejne 9 sekund bez żadnego powodu, ponieważ Menedżer pamięci podręcznej (cokolwiek to jest) nie ma świadomości, że dany konkretny element jest proszony, tylko że "coś" jest żądany i "coś" jest w toku.


Pytanie:

Czy są jakieś biblioteki buforujące dla. NET (pre-4.0), które wykonują implementują takie operacje atomowe, jakich można się spodziewać po buforze bezpiecznym dla wątków?

Lub, alternatywnie, czy istnieje jakiś sposób, aby rozszerzyć istniejącą pamięć podręczną "thread-safe", aby wspierać takie operacje, bez serializowania dostępu do pamięci podręcznej (co byłoby sprzeczne z celem użycia implementacji thread-safe w pierwszym miejsce)? Wątpię, ale może jestem po prostu zmęczony i ignoruję oczywiste obejście.

Lub... coś jeszcze mi umyka? Czy to tylko standardowa praktyka pozwalać dwóm konkurującym wątkom na parowanie się, jeśli zdarzy się, że oba żądają tego samego przedmiotu, w tym samym czasie, po raz pierwszy lub po wygaśnięciu?

Author: Aaronaught, 2010-02-25

4 answers

Znam twój ból, ponieważ jestem jednym z architektów Dedoose . Namieszałem w wielu bibliotekach buforujących i skończyłem z budowaniem tej Po wielu uciskach. Jedynym założeniem dla tego Menedżera pamięci podręcznej jest to, że wszystkie kolekcje przechowywane przez tę klasę implementują interfejs, aby uzyskać GUID jako właściwość " Id " na każdym obiekcie. Ponieważ jest to dla RIA, zawiera wiele metod dodawania /aktualizowania / usuwania elementów z tych kolekcji.

Oto mój 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;
}

Oto przykład jak go używam:

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

Oto interfejs, o którym mowa:

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

}

Mam nadzieję, że to pomoże, przeszedłem przez piekło i z powrotem kilka razy, aby w końcu dojść do tego jako rozwiązania, i dla nas było to darem niebios, ale nie mogę zagwarantować, że jest idealny, tylko, że nie znaleźliśmy jeszcze problemu.

 5
Author: JTtheGeek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-09 19:27:32

Wygląda na to, że współbieżne Kolekcje. NET 4.0 wykorzystują nowe prymitywy synchronizacji, które obracają się przed przełączeniem kontekstu, na wypadek, gdyby zasób został szybko uwolniony. Więc nadal się zamykają, tylko w bardziej oportunistyczny sposób. Jeśli uważasz, że logika pobierania danych jest krótsza niż czas, to wydaje się, że byłoby to bardzo korzystne. Ale wspomniałeś o sieci, co sprawia, że myślę, że to nie ma zastosowania.

Poczekałbym aż będziesz miał proste, zsynchronizowane rozwiązanie w umieść i zmierz wydajność i zachowanie, zanim założysz, że będziesz miał problemy z wydajnością związane z współbieżnością.

Jeśli naprawdę martwisz się o zawartość pamięci podręcznej, możesz wykorzystać istniejącą infrastrukturę pamięci podręcznej i logicznie podzielić ją na regiony. Następnie zsynchronizuj dostęp do każdego regionu niezależnie.

Przykładowa strategia jeśli zestaw danych składa się z elementów, które są kluczowane na numerycznych identyfikatorach i chcesz podzielić pamięć podręczną na regiony 10, możesz (mod 10) identyfikator określający, w którym regionie SIĘ ZNAJDUJĄ. Macie 10 obiektów do namierzenia. Cały kod można zapisać dla zmiennej liczby regionów, którą można ustawić za pomocą konfiguracji lub określić przy starcie aplikacji w zależności od całkowitej liczby elementów, które przewidujesz/zamierzasz buforować.

Jeśli trafienia w pamięci podręcznej są kluczowane w nieprawidłowy sposób, będziesz musiał wymyślić niestandardową heurystykę, aby podzielić pamięć podręczną.

Update (per comment): Było fajnie. I myślę, że poniżej znajduje się tak drobnoziarniste blokowanie, jak można mieć nadzieję, bez przechodzenia całkowicie szalony (lub utrzymanie / synchronizacja słownik zamków dla każdego klucza pamięci podręcznej). Nie testowałem więc pewnie są błędy, ale pomysł powinien być zilustrowany. Śledź listę żądanych identyfikatorów, a następnie użyj tego, aby zdecydować, czy chcesz uzyskać przedmiot samodzielnie, czy tylko czekać na zakończenie poprzedniego żądania. Oczekiwanie (i wstawianie cache) jest synchronizowane z wątkiem blokowanie i sygnalizacja za pomocą Wait i PulseAll. Dostęp do żądanej listy ID jest zsynchronizowany z ściśle określonym zakresem ReaderWriterLockSlim.

To jest pamięć podręczna tylko do odczytu. Jeśli tworzysz / aktualizujesz/usuwasz, musisz upewnić się, że usuwasz Identyfikatory z requestedIds po ich otrzymaniu (przed wywołaniem do {[5] }będziesz chciał dodać kolejną try..finally i uzyskać _requestedIdsLock write-lock). Ponadto, przy tworzeniu/aktualizacjach/usuwaniu, najprostszym sposobem zarządzania pamięcią podręczną byłoby usunięcie istniejącego elementu z _cache jeśli / kiedy podstawowa operacja create / update/delete powiedzie się.

(UPS, zobacz update 2 poniżej.)

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

Aktualizacja 2:

Źle zrozumiałem zachowanie UpgradeableReadLock... tylko jeden wątek na raz może pomieścić Upgradeableadlock. Tak więc powyższe powinno być zreformowane, aby tylko początkowo przechwytywać blokady odczytu i całkowicie z nich zrezygnować i uzyskać pełnoprawną blokadę zapisu podczas dodawania elementów do _requestedIds.

 3
Author: G-Wiz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-02-25 16:24:31

Zaimplementowałem prostą bibliotekę o nazwie MemoryCacheT. Jest na GitHub i NuGet . Zasadniczo przechowuje elementy w języku współbieżnym i możesz określić strategię wygaśnięcia podczas dodawania elementów. Wszelkie opinie, recenzje, sugestie są mile widziane.

 1
Author: Ufuk Hacıoğulları,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-23 19:48:10

W końcu wymyśliłem realne rozwiązanie tego problemu, dzięki dialogowi w komentarzach. To, co zrobiłem, to stworzenie wrappera, który jest częściowo zaimplementowaną klasą abstrakcyjną bazową, która używa dowolnej standardowej biblioteki pamięci podręcznej jako zapasowej pamięci podręcznej (wystarczy zaimplementować Contains, Get, Put, i Remove metody). W tej chwili używam do tego bloku aplikacji Buforującej EntLib i zajęło to trochę czasu, ponieważ niektóre aspekty tej biblioteki są... cóż... nie to przemyślane.

W każdym razie, całkowity kod jest teraz blisko linii 1k, więc nie zamierzam publikować całej rzeczy tutaj, ale podstawowa idea jest taka:

  1. Przechwycić wszystkie połączenia do Get, Put/Add, I Remove metody.

  2. Zamiast dodawać oryginalny element, Dodaj element "entry", który zawiera ManualResetEvent oprócz właściwości Value. Zgodnie z kilkoma radami udzielonymi mi dzisiaj na wcześniejszym pytaniu, wpis implementuje zatrzask odliczający, który jest zwiększany ilekroć wpis zostanie zdobyty i zmniejszony, ilekroć zostanie wydany. Zarówno Ładowarka, jak i wszystkie przyszłe poszukiwania biorą udział w zatrzasku odliczania, więc gdy licznik osiągnie zero, dane są gwarantowane, aby być dostępne, a ManualResetEvent jest niszczony w celu oszczędzania zasobów.

  3. Gdy wpis ma być leniwie ładowany, wpis jest tworzony i dodawany do bufora zapasowego od razu, a zdarzenie jest w stanie niesignalowanym. Kolejne wywołania nowej metody GetOrAdd lub przechwycone metody Get znajdą ten wpis i albo poczekają na zdarzenie (jeśli zdarzenie istnieje), albo zwrócą powiązaną wartość natychmiast(jeśli zdarzenie nie istnieje).

  4. Metoda Put dodaje wpis bez zdarzenia; wyglądają one tak samo jak wpisy, dla których leniwe ładowanie zostało już zakończone.

  5. Ponieważ GetOrAdd nadal implementuje Get, po którym następuje opcjonalne Put, Metoda ta jest zsynchronizowana (serializowana) z Put i Remove metody, ale tylko, Aby dodać niekompletny wpis, nie przez cały czas leniwego ładowania. Metody Get są serializowane , a nie; efektywnie cały interfejs działa jak automatyczna blokada czytnika-pisarza.

To wciąż trwa, ale przeprowadziłem tuzin testów jednostkowych i wygląda na to, że się trzyma. Zachowuje się poprawnie dla obu scenariuszy opisanych w pytaniu. Innymi słowy:
  • Wezwanie do długi czas pracy lazy-load (GetOrAdd) dla klucza X (symulowany przez Thread.Sleep), który trwa 10 sekund, a następnie inny GetOrAdddla tego samego klucza X w innym wątku dokładnie 9 sekund później, powoduje, że oba wątki otrzymują poprawne dane w tym samym czasie (10 sekund od T0). Ładunki nie są powielane.

  • Natychmiast ładuje wartość dla klucza X , następnie uruchamia długo działający lazy-load dla klucza Y , a następnie prosi klucz X w innym wątku (przed zakończeniem y), natychmiast zwraca wartość dla X . Połączenia blokujące są odizolowane od odpowiedniego klucza.

Daje również to, co myślę, że jest najbardziej intuicyjnym wynikiem, gdy rozpoczynasz leniwe Ładowanie, a następnie natychmiast usuwasz klucz z pamięci podręcznej; wątek, który pierwotnie zażądał wartości, otrzyma rzeczywistą wartość, ale wszystkie inne wątki, które zażądają tego samego klucza w dowolnym momencie po usunięciu, otrzymają rzeczywistą wartość. nic z powrotem (null) i natychmiast wrócić.

W sumie jestem z tego całkiem zadowolony. Nadal chciałbym, żeby była biblioteka, która zrobiła to za mnie, ale przypuszczam, że jeśli chcesz coś zrobić dobrze... no wiesz.

 0
Author: Aaronaught,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-02-26 00:14:34