2009-07-06 7 views
4

[EDIT]Performance en ce qui concerne la mise en œuvre <T> mises en cache IEnumerable

Le nouveau Reactive Framework résout le problème décrit ci-dessous, en utilisant la méthode d'extension System.Linq.EnumerableEx.MemoizeAll().

En interne, MemoizeAll() utilise un System.Linq.EnumerableEx.MemoizeAllEnumerable<T> (trouvé dans l'assembly System.Interactive), qui est similaire à mon ThreadSafeCachedEnumerable<T> (sorta).

Voici un exemple qui imprime drôlement arrangé le contenu d'un Enumerable (numéros 1-10) très lentement, puis imprime rapidement le contenu d'une seconde fois (car il cache les valeurs):

// Create an Enumerable<int> containing numbers 1-10, using Thread.Sleep() to simulate work 
var slowEnum = EnumerableEx.Generate(1, currentNum => (currentNum <= 10), currentNum => currentNum, previousNum => { Thread.Sleep(250); return previousNum + 1; }); 

// This decorates the slow enumerable with one that will cache each value. 
var cachedEnum = slowEnum.MemoizeAll(); 

// Print the numbers 
foreach (var num in cachedEnum.Repeat(2)) 
{ 
    Console.WriteLine(num); 
} 

[/EDIT]

Bonjour gourous multi-threading,

J'ai créé la classe ThreadSafeCachedEnumerable l'intention d'augmenter les performances où les longues requêtes en cours d'exécution où réutilisés. L'idée était d'obtenir un énumérateur à partir d'un IEnumerable et d'ajouter des éléments à un cache à chaque appel à MoveNext(). Ce qui suit est mon implémentation actuelle:

/// <summary> 
/// Wraps an IEnumerable&lt;T&gt; and provides a thread-safe means of caching the values."/> 
/// </summary> 
/// <typeparam name="T"></typeparam> 
class ThreadSafeCachedEnumerable<T> : IEnumerable<T> 
{ 
    // An enumerator from the original IEnumerable<T> 
    private IEnumerator<T> enumerator; 

    // The items we have already cached (from this.enumerator) 
    private IList<T> cachedItems = new List<T>(); 

    public ThreadSafeCachedEnumerable(IEnumerable<T> enumerable) 
    { 
     this.enumerator = enumerable.GetEnumerator(); 
    } 

    #region IEnumerable<T> Members 

    public IEnumerator<T> GetEnumerator() 
    { 
     // The index into the sequence 
     int currentIndex = 0; 

     // We will break with yield break 
     while (true) 
     { 
      // The currentIndex will never be decremented, 
      // so we can check without locking first 
      if (currentIndex < this.cachedItems.Count) 
      { 
       var current = this.cachedItems[currentIndex]; 
       currentIndex += 1; 
       yield return current; 
      } 
      else 
      { 
       // If !(currentIndex < this.cachedItems.Count), 
       // we need to synchronize access to this.enumerator 
       lock (enumerator) 
       { 
        // See if we have more cached items ... 
        if (currentIndex < this.cachedItems.Count) 
        { 
         var current = this.cachedItems[currentIndex]; 
         currentIndex += 1; 
         yield return current; 
        } 
        else 
        { 
         // ... otherwise, we'll need to get the next item from this.enumerator.MoveNext() 
         if (this.enumerator.MoveNext()) 
         { 
          // capture the current item and cache it, then increment the currentIndex 
          var current = this.enumerator.Current; 
          this.cachedItems.Add(current); 
          currentIndex += 1; 
          yield return current; 
         } 
         else 
         { 
          // We reached the end of the enumerator - we're done 
          yield break; 
         } 
        } 
       } 
      } 
     } 
    } 

    #endregion 

    #region IEnumerable Members 

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
     return this.GetEnumerator(); 
    } 

    #endregion 
} 


I simplement « verrouillage (this.enumerator) » lorsque les pas plus d'éléments semblent être dans le cache, juste au cas où un autre thread est sur le point de ajouter un autre élément (je suppose qu'appeler MoveNext() sur this.enumerator à partir de deux threads est une mauvaise idée).

La performance est grande lors de la récupération des éléments précédemment mises en cache, mais il commence à souffrir lors de l'obtention de nombreux articles pour la première fois (en raison du blocage constant). Des suggestions pour augmenter la performance?

Merci!

+0

Je ne vois ni «MemoizeAll», ni «MemoizeAllEnumerable» dans [codebase Rx] (https://github.com/Reactive-Extensions/Rx.NET). Cela étant dit, votre 'ThreadSafeCachedEnumerable' a oublié d'appeler dispose() sur l'énumérateur source, qui est un IDisposable. Ceci est habituellement la responsabilité des méthodes 'foreach' ou Linq, mais puisque maintenant vous en obtenez un, vous devriez être responsable de l'éliminer. – KFL

Répondre

7

Quelques recommandations:

  1. Il est désormais généralement admis de ne pas faire des classes de conteneurs responsables de verrouillage. Quelqu'un d'appeler votre recenseur en cache, par exemple, pourrait aussi vouloir éviter de nouvelles entrées d'être ajouté au récipient pendant l'énumération, ce qui signifie que le verrouillage se produirait deux fois. Par conséquent, il est préférable de reporter cette responsabilité à l'appelant.
  2. Votre mise en cache dépend du recenseur toujours le retour d'articles dans l'ordre, ce qui est pas garantie. Il est préférable d'utiliser un Dictionary ou HashSet. De même, les éléments peuvent être supprimés entre les appels, ce qui invalide le cache.
  3. Il est généralement recommandé de ne pas mettre en place des verrous sur les objets accessibles au public. Cela inclut l'énumérateur enveloppé. Des exceptions sont envisageables, par exemple lorsque vous êtes absolument certain que vous êtes absolument certain que vous êtes le seul exemple qui représente une référence à la classe de conteneur que vous êtes sur l'énumération. Cela invaliderait aussi largement mes objections sous # 2.
+5

+1 "Il est maintenant généralement accepté de ne pas rendre les classes de conteneur responsables du verrouillage". J'aimerais que tout le monde ait ce mémo. –

+0

Ce sont de belles recommandations - voici mes pensées: 1) Cela ajouterait trop de complexité à mes fins. Je veux juste mettre en cache les résultats des projections coûteuses de LINQ, où il se peut que je n'aie besoin que de quelques résultats. Une liste chargée paresseuse, essentiellement, uniquement sans pouvoir accéder à un élément par index. Je serais d'accord avec vous pour les conteneurs réguliers, cependant. 2) À mes fins, cela devrait être compris par l'appelant comme une mise en garde. 3) Je ne peux pas imaginer un IEnumerable contenant une référence à l'un de ses IEnumerators, mais je suppose que c'est une possibilité. Merci pour les suggestions. – Charles

+0

Pratique généralement acceptée, hein? Est-ce que cela explique le nouvel espace de noms 'System.Collections.Concurrent' dans .NET 4.0? –

2

Le verrouillage de .NET est normalement très rapide (s'il n'y a pas de conflit). Le profilage a-t-il identifié le verrouillage comme la source du problème de performance? Combien de temps faut-il pour appeler MoveNext sur l'énumérateur sous-jacent?

En outre, le code tel qu'il est n'est pas thread-safe. Vous ne pouvez pas appeler en toute sécurité this.cachedItems[currentIndex] sur un thread (dans if (currentIndex < this.cachedItems.Count)) tout en appelant this.cachedItems.Add(current) sur un autre. De la List(T) documentation: "Une liste (T) peut prendre en charge plusieurs lecteurs simultanément, tant que la collection n'est pas modifiée." Pour être thread-safe, vous devez protéger tous les accès à this.cachedItems avec un verrou (s'il y a une chance qu'un ou plusieurs threads puissent le modifier).

+0

C'est un point valide, mais savez-vous si une exception pourrait être levée? L'idée de ne pas verrouiller était que je pouvais compter sur la liste qui grossissait et que l'index devenait plus grand, donc je pouvais contourner le besoin de verrouiller if (currentIndex Charles

+0

La principale chose qui me préoccupe est un autre thread redimensionnant le tableau interne de la liste (dans Add()) tandis que le thread de lecture utilise l'indexeur pour récupérer un élément. Il semble possible qu'il puisse retourner default (T) ou lancer une ArgumentOutOfRangeException. Bien sûr, tout dépend de l'implémentation exacte de la liste , donc le mieux que je puisse dire est que le comportement est "indéfini". (Même si Reflector vous montre que ce serait sûr, qui sait si cela pourrait changer dans .NET 4.0, introduisant un bug subtil et difficile à trouver?) –