2009-04-30 19 views
8

He estado jugando con colecciones y subprocesos y encontré los ingeniosos métodos de extensión que las personas han creado para facilitar el uso de ReaderWriterLockSlim al permitir el patrón IDisposable.Método de extensión ReaderWriterLockSlim Rendimiento

Sin embargo, creo que me he dado cuenta de que algo en la implementación es un asesino de rendimiento. Me doy cuenta de que no se supone que los métodos de extensión realmente tengan un impacto en el rendimiento, por lo tanto, estoy asumiendo que algo en la implementación es la causa ... ¿la cantidad de estructuras desechables creadas/recolectadas?

Aquí hay algo de código de prueba:

using System; 
using System.Collections.Generic; 
using System.Threading; 
using System.Diagnostics; 

namespace LockPlay { 

    static class RWLSExtension { 
     struct Disposable : IDisposable { 
      readonly Action _action; 
      public Disposable(Action action) { 
       _action = action; 
      } 
      public void Dispose() { 
       _action(); 
      } 
     } // end struct 
     public static IDisposable ReadLock(this ReaderWriterLockSlim rwls) { 
      rwls.EnterReadLock(); 
      return new Disposable(rwls.ExitReadLock); 
     } 
     public static IDisposable UpgradableReadLock(this ReaderWriterLockSlim rwls) { 
      rwls.EnterUpgradeableReadLock(); 
      return new Disposable(rwls.ExitUpgradeableReadLock); 
     } 
     public static IDisposable WriteLock(this ReaderWriterLockSlim rwls) { 
      rwls.EnterWriteLock(); 
      return new Disposable(rwls.ExitWriteLock); 
     } 
    } // end class 

    class Program { 

     class MonitorList<T> : List<T>, IList<T> { 
      object _syncLock = new object(); 
      public MonitorList(IEnumerable<T> collection) : base(collection) { } 
      T IList<T>.this[int index] { 
       get { 
        lock(_syncLock) 
         return base[index]; 
       } 
       set { 
        lock(_syncLock) 
         base[index] = value; 
       } 
      } 
     } // end class 

     class RWLSList<T> : List<T>, IList<T> { 
      ReaderWriterLockSlim _rwls = new ReaderWriterLockSlim(); 
      public RWLSList(IEnumerable<T> collection) : base(collection) { } 
      T IList<T>.this[int index] { 
       get { 
        try { 
         _rwls.EnterReadLock(); 
         return base[index]; 
        } finally { 
         _rwls.ExitReadLock(); 
        } 
       } 
       set { 
        try { 
         _rwls.EnterWriteLock(); 
         base[index] = value; 
        } finally { 
         _rwls.ExitWriteLock(); 
        } 
       } 
      } 
     } // end class 

     class RWLSExtList<T> : List<T>, IList<T> { 
      ReaderWriterLockSlim _rwls = new ReaderWriterLockSlim(); 
      public RWLSExtList(IEnumerable<T> collection) : base(collection) { } 
      T IList<T>.this[int index] { 
       get { 
        using(_rwls.ReadLock()) 
         return base[index]; 
       } 
       set { 
        using(_rwls.WriteLock()) 
         base[index] = value; 
       } 
      } 
     } // end class 

     static void Main(string[] args) { 
      const int ITERATIONS = 100; 
      const int WORK = 10000; 
      const int WRITE_THREADS = 4; 
      const int READ_THREADS = WRITE_THREADS * 3; 

      // create data - first List is for comparison only... not thread safe 
      int[] copy = new int[WORK]; 
      IList<int>[] l = { new List<int>(copy), new MonitorList<int>(copy), new RWLSList<int>(copy), new RWLSExtList<int>(copy) }; 

      // test each list 
      Thread[] writeThreads = new Thread[WRITE_THREADS]; 
      Thread[] readThreads = new Thread[READ_THREADS]; 
      foreach(var list in l) { 
       Stopwatch sw = Stopwatch.StartNew(); 
       for(int k=0; k < ITERATIONS; k++) { 
        for(int i = 0; i < writeThreads.Length; i++) { 
         writeThreads[i] = new Thread(p => { 
          IList<int> il = p as IList<int>; 
          int c = il.Count; 
          for(int j = 0; j < c; j++) { 
           il[j] = j; 
          } 
         }); 
         writeThreads[i].Start(list); 
        } 
        for(int i = 0; i < readThreads.Length; i++) { 
         readThreads[i] = new Thread(p => { 
          IList<int> il = p as IList<int>; 
          int c = il.Count; 
          for(int j = 0; j < c; j++) { 
           int temp = il[j]; 
          } 
         }); 
         readThreads[i].Start(list); 
        } 
        for(int i = 0; i < readThreads.Length; i++) 
         readThreads[i].Join(); 
        for(int i = 0; i < writeThreads.Length; i++) 
         writeThreads[i].Join(); 
       }; 
       sw.Stop(); 
       Console.WriteLine("time: {0} class: {1}", sw.Elapsed, list.GetType()); 
      } 
      Console.WriteLine("DONE"); 
      Console.ReadLine(); 
     } 
    } // end class 
} // end namespace 

Esto es un resultado típico:

time: 00:00:03.0965242 class: System.Collections.Generic.List`1[System.Int32] 
time: 00:00:11.9194573 class: LockPlay.Program+MonitorList`1[System.Int32] 
time: 00:00:08.9510258 class: LockPlay.Program+RWLSList`1[System.Int32] 
time: 00:00:16.9888435 class: LockPlay.Program+RWLSExtList`1[System.Int32] 
DONE

Como se puede ver, el uso de las extensiones en realidad hace que la actuación peor que simplemente usar lock (monitor) .

Respuesta

9

Parece que es el precio de crear instancias de millones de estructuras y el resto de las invocaciones.

Me atrevería a decir que el ReaderWriterLockSlim está siendo mal utilizado en este ejemplo, un bloqueo es lo suficientemente bueno en este caso y la ventaja de rendimiento que obtiene con el ReaderWriterLockSlim es insignificante en comparación con el precio de explicar estos conceptos a desarrolladores junior.

Obtiene una gran ventaja de con bloqueos de estilo de escritura del lector cuando se tarda una cantidad de tiempo no despreciable para realizar lecturas y escrituras. El impulso será mayor cuando tienes un sistema predominantemente basado en lectura.

Intente insertar un Thread.Sleep (1) mientras se adquieren los bloqueos para ver la gran diferencia que hace.

Ver este punto de referencia:

 
Time for Test.SynchronizedList`1[System.Int32] Time Elapsed 12310 ms 
Time for Test.ReaderWriterLockedList`1[System.Int32] Time Elapsed 547 ms 
Time for Test.ManualReaderWriterLockedList`1[System.Int32] Time Elapsed 566 ms 

En mi evaluación comparativa que realmente no noto mucha diferencia entre los dos estilos, me sentiría cómodo con él siempre que tenía algún tipo de protección finalizador en caso de que la gente se olvide de disponer ....

using System.Threading; 
using System.Diagnostics; 
using System.Collections.Generic; 
using System; 
using System.Linq; 

namespace Test { 

    static class RWLSExtension { 
     struct Disposable : IDisposable { 
      readonly Action _action; 
      public Disposable(Action action) { 
       _action = action; 
      } 
      public void Dispose() { 
       _action(); 
      } 
     } 

     public static IDisposable ReadLock(this ReaderWriterLockSlim rwls) { 
      rwls.EnterReadLock(); 
      return new Disposable(rwls.ExitReadLock); 
     } 
     public static IDisposable UpgradableReadLock(this ReaderWriterLockSlim rwls) { 
      rwls.EnterUpgradeableReadLock(); 
      return new Disposable(rwls.ExitUpgradeableReadLock); 
     } 
     public static IDisposable WriteLock(this ReaderWriterLockSlim rwls) { 
      rwls.EnterWriteLock(); 
      return new Disposable(rwls.ExitWriteLock); 
     } 
    } 

    class SlowList<T> { 

     List<T> baseList = new List<T>(); 

     public void AddRange(IEnumerable<T> items) { 
      baseList.AddRange(items); 
     } 

     public virtual T this[int index] { 
      get { 
       Thread.Sleep(1); 
       return baseList[index]; 
      } 
      set { 
       baseList[index] = value; 
       Thread.Sleep(1); 
      } 
     } 
    } 

    class SynchronizedList<T> : SlowList<T> { 

     object sync = new object(); 

     public override T this[int index] { 
      get { 
       lock (sync) { 
        return base[index]; 
       } 

      } 
      set { 
       lock (sync) { 
        base[index] = value; 
       } 
      } 
     } 
    } 


    class ManualReaderWriterLockedList<T> : SlowList<T> { 

     ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); 

     public override T this[int index] { 
      get { 
       T item; 
       try { 
        slimLock.EnterReadLock(); 
        item = base[index]; 
       } finally { 
        slimLock.ExitReadLock(); 
       } 
       return item; 

      } 
      set { 
       try { 
        slimLock.EnterWriteLock(); 
        base[index] = value; 
       } finally { 
        slimLock.ExitWriteLock(); 
       } 
      } 
     } 
    } 

    class ReaderWriterLockedList<T> : SlowList<T> { 

     ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); 

     public override T this[int index] { 
      get { 
       using (slimLock.ReadLock()) { 
        return base[index]; 
       } 
      } 
      set { 
       using (slimLock.WriteLock()) { 
        base[index] = value; 
       } 
      } 
     } 
    } 


    class Program { 


     private static void Repeat(int times, int asyncThreads, Action action) { 
      if (asyncThreads > 0) { 

       var threads = new List<Thread>(); 

       for (int i = 0; i < asyncThreads; i++) { 

        int iterations = times/asyncThreads; 
        if (i == 0) { 
         iterations += times % asyncThreads; 
        } 

        Thread thread = new Thread(new ThreadStart(() => Repeat(iterations, 0, action))); 
        thread.Start(); 
        threads.Add(thread); 
       } 

       foreach (var thread in threads) { 
        thread.Join(); 
       } 

      } else { 
       for (int i = 0; i < times; i++) { 
        action(); 
       } 
      } 
     } 

     static void TimeAction(string description, Action func) { 
      var watch = new Stopwatch(); 
      watch.Start(); 
      func(); 
      watch.Stop(); 
      Console.Write(description); 
      Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds); 
     } 

     static void Main(string[] args) { 

      int threadCount = 40; 
      int iterations = 200; 
      int readToWriteRatio = 60; 

      var baseList = Enumerable.Range(0, 10000).ToList(); 

      List<SlowList<int>> lists = new List<SlowList<int>>() { 
       new SynchronizedList<int>() , 
       new ReaderWriterLockedList<int>(), 
       new ManualReaderWriterLockedList<int>() 
      }; 

      foreach (var list in lists) { 
       list.AddRange(baseList); 
      } 


      foreach (var list in lists) { 
       TimeAction("Time for " + list.GetType().ToString(),() => 
       { 
        Repeat(iterations, threadCount,() => 
        { 
         list[100] = 99; 
         for (int i = 0; i < readToWriteRatio; i++) { 
          int ignore = list[i]; 
         } 
        }); 
       }); 
      } 



      Console.WriteLine("DONE"); 
      Console.ReadLine(); 
     } 
    } 
} 
+0

El olvidarse de llamar a Dispose es idéntica a olvidar para desbloquear, y en ese caso retrasa y la finalización no determinista es muy poco probable que ayude a prevenir los bloqueos muertos que siguen. Peor aún, es posible (probable) que haga que los bloqueos muertos sean intermitentes y casi imposibles de depurar. Deje el finalizador y asegúrese de obtener un interbloqueo de depuración. – wekempf

+0

@wekempf ver: http://gist.github.com/104477 efecto secundario es que debe ser una clase descartable para que esto funcione al menos en DEPURTO –

+0

@ sambo99: puedo ver el razonamiento allí, pero todavía no lo hago de acuerdo. En el mejor de los casos, ha convertido dead lock en dead lock + eventual assert, sin ningún beneficio adicional (es decir, no es más fácil de depurar). Todo por un error que parece ALTAMENTE improbable que se me ocurra (especialmente si cambia los nombres de los métodos o los convierte en métodos que no son de extensión). – wekempf

2

Mi conjetura (lo que se necesita para perfilar para verificar) es que la caída de rendimiento no es desde la creación de las instancias desechables (que debe ser bastante barato, que son estructuras). En cambio, espero que sea al crear los delegados de Acción. Podría intentar cambiar la implementación de su estructura Desechable para almacenar la instancia de ReaderWriterLockSlim en lugar de crear un delegado de Acción.

Editar: @ 280Z28 La publicación confirma que es la asignación de montón de delegados de acción que está causando la desaceleración.

7

Parece que el código utiliza una estructura para evitar la sobrecarga de creación de objetos, pero no toma los demás pasos necesarios para mantener este peso ligero. Creo que recubre el valor de retorno desde ReadLock, y si es así, niega toda la ventaja de la estructura. Esto debería solucionar todos los problemas y funcionar tan bien como no pasar por la interfaz IDisposable.

Editar: Exitos de referencia exigidos.Estos resultados están normalizados, por lo que el método manual (llamada Enter/ExitReadLock y Enter/ExitWriteLock en línea con el código protegido) tiene un valor de tiempo de 1.00. El método original es lento porque asigna objetos en el montón que el método manual no. Solucioné este problema y, en el modo de lanzamiento, incluso la sobrecarga de llamadas al método de extensión desaparece, dejándola igual de rápido que el método manual.

depuración de construcción: Construir

Manual:    1.00 
Original Extensions: 1.62 
My Extensions:  1.24 

lanzamiento:

Manual:    1.00 
Original Extensions: 1.51 
My Extensions:  1.00 

Mi código:

internal static class RWLSExtension 
{ 
    public static ReadLockHelper ReadLock(this ReaderWriterLockSlim readerWriterLock) 
    { 
     return new ReadLockHelper(readerWriterLock); 
    } 

    public static UpgradeableReadLockHelper UpgradableReadLock(this ReaderWriterLockSlim readerWriterLock) 
    { 
     return new UpgradeableReadLockHelper(readerWriterLock); 
    } 

    public static WriteLockHelper WriteLock(this ReaderWriterLockSlim readerWriterLock) 
    { 
     return new WriteLockHelper(readerWriterLock); 
    } 

    public struct ReadLockHelper : IDisposable 
    { 
     private readonly ReaderWriterLockSlim readerWriterLock; 

     public ReadLockHelper(ReaderWriterLockSlim readerWriterLock) 
     { 
      readerWriterLock.EnterReadLock(); 
      this.readerWriterLock = readerWriterLock; 
     } 

     public void Dispose() 
     { 
      this.readerWriterLock.ExitReadLock(); 
     } 
    } 

    public struct UpgradeableReadLockHelper : IDisposable 
    { 
     private readonly ReaderWriterLockSlim readerWriterLock; 

     public UpgradeableReadLockHelper(ReaderWriterLockSlim readerWriterLock) 
     { 
      readerWriterLock.EnterUpgradeableReadLock(); 
      this.readerWriterLock = readerWriterLock; 
     } 

     public void Dispose() 
     { 
      this.readerWriterLock.ExitUpgradeableReadLock(); 
     } 
    } 

    public struct WriteLockHelper : IDisposable 
    { 
     private readonly ReaderWriterLockSlim readerWriterLock; 

     public WriteLockHelper(ReaderWriterLockSlim readerWriterLock) 
     { 
      readerWriterLock.EnterWriteLock(); 
      this.readerWriterLock = readerWriterLock; 
     } 

     public void Dispose() 
     { 
      this.readerWriterLock.ExitWriteLock(); 
     } 
    } 
} 
+1

Ni siquiera describió su código, ni explicó por qué su –

+0

lento Mi código no crea ningún nuevo objeto asignado por el montón, por lo que incluso si el tiempo es idéntico en un punto de referencia corto, este método pone menos estrés en el administrador de memoria más tarde. –

Cuestiones relacionadas