2009-04-30 11 views
8

Ich habe mit Sammlungen und Threading gespielt und bin auf die raffinierten Erweiterungsmethoden gestoßen, die Leute erstellt haben, um die Verwendung von ReaderWriterLockSlim zu erleichtern, indem das IDisposable-Muster erlaubt wird.ReaderWriterLockSlim Erweiterungsmethode Leistung

Ich glaube jedoch, dass ich zu der Erkenntnis gekommen bin, dass etwas in der Implementierung ein Performance-Killer ist. Mir ist klar, dass Erweiterungsmethoden die Leistung nicht wirklich beeinflussen sollen. Daher nehme ich an, dass etwas in der Implementierung die Ursache ist ... die Menge der geschaffenen/gesammelten Einwegstrukturen?

Hier einige Testcode:

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 

Hier ist ein typisches Ergebnis:

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

Wie Sie die Erweiterungen sehen können, indem tatsächlich die Leistung macht WORSE als nur mit lock (Monitor) .

Antwort

9

Sieht aus wie der Preis für die Instanziierung von Millionen von Strukturen und das zusätzliche Stück von Invokationen.

Ich würde so weit gehen zu sagen, dass der ReaderWriterLockSlim in diesem Beispiel missbraucht wird, eine Sperre ist in diesem Fall gut genug und die Leistung, die Sie mit dem ReaderWriterLockSlim erhalten, ist im Vergleich zu dem Preis der Erklärung dieser Konzepte zu vernachlässigen Junior-Entwickler.

Sie erhalten eine große Vorteil mit Reader Writer Stil Schlösser, wenn es eine nicht vernachlässigbare Zeit für Lese- und Schreibvorgänge benötigt. Der Boost wird am größten sein, wenn Sie ein System haben, das hauptsächlich auf Leseoperationen basiert.

Versuchen Sie, ein Thread.Sleep (1) einzufügen, während die Sperren erfasst werden, um zu sehen, wie groß der Unterschied ist.

diesen Benchmark Siehe:

 
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 

In meinem Benchmarking ich nicht wirklich zwischen den beiden Arten viel Unterschied bemerken, würde ich fühle mich wohl mit ihm, vorausgesetzt, es einige Finalizerthread Schutz bei Menschen hatten zu entsorgen vergessen ....

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

vergessend entsorgen zu vergessen identisch zu nennen, ist zu entsperren, und in diesem Fall verzögert und nicht-deterministische Finalisierung ist höchst unwahrscheinlich, um die folgenden toten Sperren zu verhindern. Schlimmer noch, es ist (wahrscheinlich) möglich, dass die toten Sperren intermittierend und fast unmöglich zu debuggen sind. Lassen Sie den Finalizer aus und stellen Sie sicher, dass wir einen debugbaren Deadlock erhalten. – wekempf

+0

@wekempf siehe: http://gist.github.com/104477 Nebeneffekt ist, dass Einweg muss eine Klasse sein, damit dies zumindest in DEBUG –

+0

@ Sambo99 funktioniert: Ich kann die Argumentation dort sehen, aber immer noch nicht zustimmen. Im besten Fall haben Sie dead lock in dead lock + eventual assert umgewandelt, ohne zusätzlichen Nutzen (d. H. Es ist nicht einfacher zu debuggen). Alles für einen Fehler, der HOCH wahrscheinlich unwahrscheinlich erscheint (besonders wenn Sie entweder die Namen der Methoden ändern oder sie zu Nicht-Erweiterungsmethoden machen). – wekempf

2

Meine Vermutung ist (Sie würde überprüfen müssen) profilieren, dass der Leistungsabfall der Einweg-Instanzen nicht von der Erstellung ist (sollten sie ziemlich billig sein, wobei structs). Stattdessen erwarte ich, dass die Action-Delegierten erstellt werden. Sie könnten versuchen, die Implementierung Ihrer Disposable-Struktur zu ändern, um die Instanz von ReaderWriterLockSlim zu speichern, anstatt einen Aktionsdelegaten zu erstellen.

Bearbeiten: @ 280Z28 Post bestätigt, dass es die Heap-Zuweisung von Aktion Delegaten ist, die die Verlangsamung verursacht.

7

Der Code scheint eine Struktur zu verwenden, um einen Overhead bei der Objektgenerierung zu vermeiden, führt jedoch nicht die anderen notwendigen Schritte aus, um diesen Lightweight zu erhalten. Ich glaube, es enthält den Rückgabewert von ReadLock, und wenn dies der Fall ist, negiert der gesamte Vorteil der Struktur. Dies sollte alle Probleme beheben und so gut wie nicht durch die IDisposable Schnittstelle ausgeführt werden.

Bearbeiten: Benchmarks gefordert.Diese Ergebnisse sind normalisiert, so dass die manuelle Methode (Aufruf Enter/ExitReadLock und Enter/ExitWriteLock Inline mit dem geschützten Code) einen Zeitwert von 1,00 haben. Die ursprüngliche Methode ist langsam, da sie Objekte auf dem Heap reserviert, die die manuelle Methode nicht verwendet. Ich habe dieses Problem behoben, und im Freigabemodus geht sogar der Overhead der Erweiterungsmethode weg und lässt ihn genauso schnell zurück wie die manuelle Methode.

Debug-Version:

Manual:    1.00 
Original Extensions: 1.62 
My Extensions:  1.24 

Releasebuild:

Manual:    1.00 
Original Extensions: 1.51 
My Extensions:  1.00 

Mein Code:

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

Sie haben nicht einmal Ihren Code profiliert, noch haben Sie erklärt, warum sein langsamer –

+0

Mein Code erstellt keine neuen Heap allokierten Objekte, so dass, selbst wenn die Zeit auf einem kurzen Benchmark identisch ist, diese Methode weniger Belastung für die Speichermanager später. –