it-swarm.com.de

Wann ist ReaderWriterLockSlim besser als ein einfaches Schloss?

Ich mache mit diesem Code einen sehr dummen Benchmark für das ReaderWriterLock, bei dem das Lesen 4x häufiger als beim Schreiben geschieht:

class Program
{
    static void Main()
    {
        ISynchro[] test = { new Locked(), new RWLocked() };

        Stopwatch sw = new Stopwatch();

        foreach ( var isynchro in test )
        {
            sw.Reset();
            sw.Start();
            Thread w1 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w1.Start( isynchro );

            Thread w2 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w2.Start( isynchro );

            Thread r1 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r1.Start( isynchro );

            Thread r2 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r2.Start( isynchro );

            w1.Join();
            w2.Join();
            r1.Join();
            r2.Join();
            sw.Stop();

            Console.WriteLine( isynchro.ToString() + ": " + sw.ElapsedMilliseconds.ToString() + "ms." );
        }

        Console.WriteLine( "End" );
        Console.ReadKey( true );
    }

    static void ReadThread(Object o)
    {
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 500; i++ )
        {
            Int32? value = synchro.Get( i );
            Thread.Sleep( 50 );
        }
    }

    static void WriteThread( Object o )
    {
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 125; i++ )
        {
            synchro.Add( i );
            Thread.Sleep( 200 );
        }
    }

}

interface ISynchro
{
    void Add( Int32 value );
    Int32? Get( Int32 index );
}

class Locked:List<Int32>, ISynchro
{
    readonly Object locker = new object();

    #region ISynchro Members

    public new void Add( int value )
    {
        lock ( locker ) 
            base.Add( value );
    }

    public int? Get( int index )
    {
        lock ( locker )
        {
            if ( this.Count <= index )
                return null;
            return this[ index ];
        }
    }

    #endregion
    public override string ToString()
    {
        return "Locked";
    }
}

class RWLocked : List<Int32>, ISynchro
{
    ReaderWriterLockSlim locker = new ReaderWriterLockSlim();

    #region ISynchro Members

    public new void Add( int value )
    {
        try
        {
            locker.EnterWriteLock();
            base.Add( value );
        }
        finally
        {
            locker.ExitWriteLock();
        }
    }

    public int? Get( int index )
    {
        try
        {
            locker.EnterReadLock();
            if ( this.Count <= index )
                return null;
            return this[ index ];
        }
        finally
        {
            locker.ExitReadLock();
        }
    }

    #endregion

    public override string ToString()
    {
        return "RW Locked";
    }
}

Aber ich verstehe, dass beide mehr oder weniger auf dieselbe Weise auftreten:

Locked: 25003ms.
RW Locked: 25002ms.
End

Selbst wenn der Lesevorgang 20 Mal so oft gemacht wird, dass er schreibt, ist die Leistung immer noch (fast) gleich. 

Mache ich hier etwas falsch?

Mit freundlichen Grüßen.

70
vtortola

In Ihrem Beispiel bedeutet der Schlaf, dass im Allgemeinen keine Konflikte bestehen. Eine unkontrollierte Sperre ist sehr schnell. Um dies zu erreichen, benötigen Sie eine contended -Sperre. Wenn schreibt in diesem Konflikt vorhanden ist, sollten sie ungefähr gleich sein (lock kann sogar schneller sein) - aber wenn meistens gelesen wird (mit einem Schreibkonflikt selten), würde ich die ReaderWriterLockSlim-Sperre erwarten um die lock zu übertreffen.

Ich persönlich ziehe eine andere Strategie vor, indem ich Referenz-Swapping verwende - also lesen Lesevorgänge immer lesen, ohne sie zu überprüfen/zu sperren/usw. zu ändern. Schreibvorgänge ändern ihre Änderungen in einer cloned -Kopie und verwenden dann Interlocked.CompareExchange, um die Referenz auszutauschen (erneut anwenden) ihre Änderung, wenn ein anderer Thread die Referenz in der Zwischenzeit verändert hat).

93
Marc Gravell

Meine eigenen Tests zeigen an, dass ReaderWriterLockSlim im Vergleich zu einer normalen lock etwa den 5-fachen Overhead hat. Für die RWLS bedeutet dies, dass die folgenden Bedingungen im Allgemeinen auftreten, wenn sie eine einfache alte Sperre übertreffen.

  • Die Anzahl der Leser liegt deutlich über der Zahl der Autoren.
  • Das Schloss müsste lange genug gehalten werden, um den zusätzlichen Aufwand zu überwinden.

In den meisten realen Anwendungen reichen diese beiden Bedingungen nicht aus, um diesen zusätzlichen Aufwand zu überwinden. Speziell in Ihrem Code werden die Sperren für eine so kurze Zeit gehalten, dass der Sperroverhead wahrscheinlich der dominierende Faktor sein wird. Wenn Sie diese Thread.Sleep-Aufrufe innerhalb des Schlosses verschieben, erhalten Sie wahrscheinlich ein anderes Ergebnis.

20
Brian Gideon

Es gibt keine Konflikte in diesem Programm. Die Get- und Add-Methoden werden in wenigen Nanosekunden ausgeführt. Die Wahrscheinlichkeit, dass mehrere Threads diese Methoden zum genauen Zeitpunkt treffen, ist verschwindend gering.

Fügen Sie einen Thread.Sleep (1) -Aufruf ein und entfernen Sie den Schlaf aus den Threads, um den Unterschied zu sehen.

14
Hans Passant

Edit 2: Einfach die Thread.Sleep-Aufrufe von ReadThread und WriteThread entfernen, sah ich Locked outperform RWLocked. Ich glaube, Hans hat hier den Nagel auf den Kopf getroffen; Ihre Methoden sind zu schnell und erzeugen keine Konflikte. Wenn ich Thread.Sleep(1) zu den Get- und Add-Methoden von Locked und RWLocked hinzufügte (und 4 Lesethreads gegen einen Schreibthread verwendete), schlug RWLocked die Hose von Locked ab.


Edit: OK, wenn ich tatsächlich dachte, als ich diese Antwort zum ersten Mal veröffentlichte, hätte ich zumindest verstanden, warum Sie die Thread.Sleep-Aufrufe dorthin gestellt haben: Sie haben versucht, die Lese-Szenarien treten häufiger auf als Schreiben. Das ist einfach nicht der richtige Weg. Stattdessen würde ich Ihren Add- und Get-Methoden einen zusätzlichen Overhead hinzufügen, um eine größere Wahrscheinlichkeit von Konflikten (wie Hans schlug ) zu schaffen, mehr Read-Threads als Write-Threads zu erstellen (um häufiges Lesen als Schreiben zu gewährleisten) und Entfernen Sie die Thread.Sleep-Aufrufe von ReadThread und WriteThread (die tatsächlich [] -Konflikt reduzieren, um das Gegenteil von dem zu erreichen, was Sie möchten).


Ich mag, was Sie bisher gemacht haben. Aber hier sind ein paar Probleme, die ich sofort sehe:

  1. Warum ruft der Thread.Sleep auf? Diese erhöhen lediglich Ihre Ausführungszeiten um einen konstanten Betrag, wodurch die Performance-Ergebnisse künstlich konvergieren.
  2. Ich würde auch nicht die Erstellung neuer Thread-Objekte in den Code einbeziehen, der von Ihrer Stopwatch gemessen wird. Das ist kein triviales Objekt.

Ob Sie einen signifikanten Unterschied feststellen werden, wenn Sie die beiden oben genannten Probleme angehen, weiß ich nicht. Ich glaube jedoch, dass sie angesprochen werden sollten, bevor die Diskussion fortgesetzt wird.

9
Dan Tao

Mit ReaderWriterLockSlim erhalten Sie eine bessere Leistung als mit einer einfachen Sperre, wenn Sie einen Teil des Codes sperren, dessen Ausführung länger dauert. In diesem Fall können Leser parallel arbeiten. Der Erwerb einer ReaderWriterLockSlim dauert länger als die Eingabe einer einfachen Monitor. Überprüfen Sie meine ReaderWriterLockTiny-Implementierung auf eine Reader-Writer-Sperre, die noch schneller als eine einfache Sperranweisung ist und eine Reader-Writer-Funktionalität bietet: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for -Netz/

8
i255

Schauen Sie sich diesen Artikel an: http://blogs.msdn.com/b/pedram/archive/2007/10/07/a-performance-comparison-of-readerwriterlockslim-with-readerwriterlock.aspx

Ihr Schlaf ist wahrscheinlich lang genug, um das Sperren/Entsperren statistisch unbedeutend zu machen.

3

Unkontrolliert Sperren benötigen Mikrosekunden, um die Ausführungszeit zu erreichen, sodass Ihre Ausführungszeit durch Ihre Aufrufe an Sleep in den Schatten gestellt wird.

3
MSN

Wenn Sie nicht über Multicore-Hardware verfügen (oder zumindest dieselbe wie Ihre geplante Produktionsumgebung), erhalten Sie hier keinen realistischen Test.

Ein sinnvollerer Test wäre die Verlängerung der Lebensdauer Ihrer gesperrten Operationen, indem Sie eine kurze Verzögerung inside the lock setzen. Auf diese Weise sollten Sie in der Lage sein, den mit ReaderWriterLockSlim hinzugefügten Parallelismus der von basic lock() implizierten Serialisierung gegenüberzustellen. 

Derzeit geht die Zeit, die Ihre gesperrten Vorgänge in Anspruch nehmen, durch das Geräusch verloren, das von den Sleep-Aufrufen außerhalb der Sperren verursacht wird. Die Gesamtzeit ist in beiden Fällen meistens auf den Schlaf bezogen.

Sind Sie sicher, dass Ihre reale App gleich viele Lese- und Schreibvorgänge hat? ReaderWriterLockSlim ist wirklich besser für den Fall, dass Sie viele Leser und relativ seltene Autoren haben. Ein Writer-Thread gegenüber 3 Reader-Threads sollte die Vorteile von ReaderWriterLockSlim besser demonstrieren. In jedem Fall sollte Ihr Test jedoch Ihrem erwarteten Zugriffsmuster entsprechen.

3
Steve Townsend

Ich denke, das liegt am Schlaf, den Sie in Ihren Leser- und Verfasser-Threads haben.
Ihr gelesener Thread hat einen 500-ms-Ruhezustand von 500 Sekunden, also 25000 Meistens schläft er.

2
Itay Karo

Wann ist ReaderWriterLockSlim besser als ein einfaches Schloss?

Wenn Sie wesentlich mehr Lesevorgänge als Schreibvorgänge haben. 

0
Illidan