Wann ist ReaderWriterLockSlim besser als ein einfaches Schloss?

78

Ich mache mit diesem Code einen sehr dummen Benchmark auf dem ReaderWriterLock, bei dem das Lesen viermal häufiger erfolgt als das Schreiben:

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 gleich abschneiden:

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

Selbst wenn das Lesen 20 Mal häufiger als das Schreiben erfolgt, ist die Leistung immer noch (fast) gleich.

Mache ich hier etwas falsch?

Mit freundlichen Grüßen.

vtortola
quelle
3
Übrigens, wenn ich den Schlaf herausnehme: "Gesperrt: 89 ms. RW gesperrt: 32 ms." - oder die Zahlen erhöhen: "Gesperrt: 1871 ms. RW gesperrt: 2506 ms."
Marc Gravell
1
Wenn ich den Schlaf entferne, ist gesperrt schneller als rwlocked
vtortola
1
Dies liegt daran, dass der Code, den Sie synchronisieren, zu schnell ist, um Konflikte zu erzeugen. Siehe Hans 'Antwort und meine Bearbeitung.
Dan Tao

Antworten:

105

In Ihrem Beispiel bedeutet der Schlaf, dass im Allgemeinen keine Konflikte bestehen. Eine unbestrittene Sperre ist sehr schnell. Damit dies von Bedeutung ist, benötigen Sie eine umstrittene Sperre. Wenn sich in dieser Konkurrenz Schreibvorgänge befinden , sollten diese ungefähr gleich sein ( lockmöglicherweise sogar schneller). Wenn es sich jedoch hauptsächlich um Lesevorgänge handelt (bei Schreibkonflikten selten), würde ich erwarten, dass die ReaderWriterLockSlimSperre die Leistung übertrifft lock.

Persönlich bevorzuge ich hier eine andere Strategie, bei der der Referenzaustausch verwendet wird. Daher können Lesevorgänge immer gelesen werden, ohne jemals zu überprüfen / zu sperren / usw. Schreibvorgänge nehmen ihre Änderung an einer geklonten Kopie vor und verwenden sie dann Interlocked.CompareExchangezum Austauschen der Referenz (erneutes Anwenden ihrer Änderung, wenn ein anderer Thread verwendet wird die Referenz in der Zwischenzeit mutiert).

Marc Gravell
quelle
1
Copy-and-Swap-on-Write ist definitiv der richtige Weg hierher.
LBushkin
24
Durch das nicht sperrende Kopieren und Austauschen beim Schreiben werden Ressourcenkonflikte oder Sperren für Leser vollständig vermieden. Es ist schnell für Autoren, wenn es keine Konflikte gibt, kann aber bei Vorhandensein von Konflikten stark blockiert werden. Es kann gut sein, wenn Autoren vor dem Erstellen der Kopie eine Sperre erwerben (die Leser interessieren sich nicht für die Sperre) und die Sperre nach dem Tausch aufheben. Wenn andernfalls jeweils 100 Threads eine gleichzeitige Aktualisierung versuchen, müssen sie im schlimmsten Fall durchschnittlich jeweils etwa 50 Kopier-Aktualisierungs-Versuch-Austausch-Operationen ausführen (insgesamt 5.000 Kopier-Aktualisierungs-Versuch-Austausch-Operationen, um 100 Aktualisierungen durchzuführen).
Supercat
1
Hier ist, wie ich denke, dass es aussehen sollte, bitte sagen Sie mir, wenn falsch:bool bDone=false; while(!bDone) { object origObj = theObj; object newObj = origObj.DeepCopy(); // then make changes to newObj if(Interlocked.CompareExchange(ref theObj, tempObj, newObj)==origObj) bDone=true; }
mcmillab
1
@ Mcmillab im Grunde ja; In einigen einfachen Fällen (normalerweise beim Umschließen eines einzelnen Werts oder wenn es keine Rolle spielt, ob später überschriebene Dinge geschrieben werden, die sie nie gesehen haben) können Sie sogar davonkommen, das zu verlieren Interlockedund nur ein volatileFeld zu verwenden und es einfach zuzuweisen - aber Wenn Sie sicher sein müssen , dass Ihr Wechselgeld nicht vollständig verloren gegangen ist, Interlockedist dies ideal
Marc Gravell
3
@ jnm2: Die Sammlungen verwenden häufig CompareExchange, aber ich glaube nicht, dass sie viel kopieren. CAS + -Sperre ist nicht unbedingt redundant, wenn man sie verwendet, um die Möglichkeit zu berücksichtigen, dass nicht alle Autoren eine Sperre erwerben. Beispielsweise kann eine Ressource, für die Konflikte im Allgemeinen selten sind, aber gelegentlich schwerwiegend sein können, eine Sperre und ein Flag aufweisen, das angibt, ob Autoren sie erwerben sollen. Wenn die Flagge klar ist, machen die Autoren einfach ein CAS und wenn es gelingt, machen sie sich auf den Weg. Ein Writer, der CAS mehr als zweimal nicht besteht, kann jedoch das Flag setzen. Die Flagge könnte dann gesetzt bleiben ...
Supercat
23

Meine eigenen Tests haben ergeben, dass ReaderWriterLockSlimder Overhead im Vergleich zu normalen Tests etwa das Fünffache beträgt lock. Das bedeutet, dass die RWLS eine einfache alte Sperre übertreffen und im Allgemeinen die folgenden Bedingungen auftreten würden.

  • Die Anzahl der Leser ist den Autoren deutlich überlegen.
  • 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. In Ihrem Code werden die Sperren so kurz gehalten, dass der Overhead der Sperren wahrscheinlich der dominierende Faktor ist. Wenn Sie diese Thread.SleepAnrufe innerhalb des Schlosses verschieben würden, würden Sie wahrscheinlich ein anderes Ergebnis erhalten.

Brian Gideon
quelle
17

Es gibt keinen Streit in diesem Programm. Die Methoden Get und Add werden in wenigen Nanosekunden ausgeführt. Die Wahrscheinlichkeit, dass mehrere Threads genau zum richtigen Zeitpunkt auf diese Methoden 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 erkennen.

Hans Passant
quelle
12

Bearbeiten 2 : Entfernen Sie einfach die Thread.SleepAnrufe von ReadThreadund WriteThread, ich sah LockedOutperform RWLocked. Ich glaube, Hans hat hier den Nagel auf den Kopf getroffen; Ihre Methoden sind zu schnell und verursachen keine Konflikte. Wenn ich Thread.Sleep(1)zu Getund AddMethoden von Lockedund hinzugefügt habe RWLocked(und 4 Lese-Threads gegen 1 Schreib-Thread verwendet habe), habe ich RWLockeddie Hose ausgezogen Locked.


Bearbeiten : OK, wenn ich tatsächlich darüber nachgedacht hätte, als ich diese Antwort zum ersten Mal gepostet habe, wäre mir zumindest klar geworden, warum Sie die Thread.SleepAnrufe dort getätigt haben: Sie haben versucht, das Szenario zu reproduzieren, dass Lesevorgänge häufiger als Schreibvorgänge stattfinden. Dies ist einfach nicht der richtige Weg, dies zu tun. Stattdessen würde ich Ihren Addund Ihren GetMethoden zusätzlichen Overhead hinzufügen, um eine größere Wahrscheinlichkeit von Konflikten zu schaffen (wie Hans vorgeschlagen hat ), mehr Lesethreads als Schreibthreads zu erstellen ( um häufigeres Lesen als Schreiben zu gewährleisten) und die Thread.SleepAufrufe von ReadThreadund WriteThread(die tatsächlich ) entfernen Reduzieren Sie Konflikte und erreichen Sie das Gegenteil von dem, was Sie wollen.


Mir gefällt, was Sie bisher gemacht haben. Aber hier sind ein paar Probleme, die ich auf Anhieb sehe:

  1. Warum die Thread.SleepAnrufe? Diese erhöhen lediglich Ihre Ausführungszeiten um einen konstanten Betrag, wodurch die Leistungsergebnisse künstlich konvergieren.
  2. Ich würde auch nicht die Erstellung neuer ThreadObjekte in den Code aufnehmen, der von Ihnen gemessen wird Stopwatch. Das ist kein triviales Objekt.

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

Dan Tao
quelle
+1: Guter Punkt auf # 2 - es kann die Ergebnisse wirklich verzerren (gleicher Unterschied zwischen den einzelnen Zeiten, aber unterschiedlicher Anteil). Wenn Sie dies jedoch lange genug ausführen, wird die Zeit zum Erstellen der ThreadObjekte allmählich unbedeutend.
Nelson Rothermel
9

Sie erzielen ReaderWriterLockSlimeine 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 die Leser parallel arbeiten. Das Erwerben von a ReaderWriterLockSlimdauert länger als das Eingeben eines einfachen Monitor. Überprüfen Sie meine ReaderWriterLockTinyImplementierung auf eine Readers-Writer-Sperre, die noch schneller als eine einfache Lock-Anweisung ist und eine Readers-Writer-Funktionalität bietet: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/

i255
quelle
Ich mag das. In diesem Artikel frage ich mich, ob Monitor auf jeder Plattform schneller als ReaderWriterLockTiny ist.
jnm2
Während ReaderWriterLockTiny schneller als ReaderWriterLockSlim (und auch Monitor) ist, gibt es einen Nachteil: Wenn es viele Leser gibt, die einige Zeit in Anspruch nehmen, gab es Autoren keine Chance (sie werden fast unbegrenzt warten) .ReaderWriterLockSlim hingegen schaffen es Ausgewogener Zugriff auf gemeinsam genutzte Ressourcen für Leser / Autoren.
Tigrou
3

Unbestrittene Sperren müssen in der Größenordnung von Mikrosekunden erfasst werden, sodass Ihre Ausführungszeit durch Ihre Aufrufe an in den Schatten gestellt wird Sleep.

MSN
quelle
3

Wenn Sie nicht über Multicore-Hardware verfügen (oder zumindest mit Ihrer geplanten Produktionsumgebung), erhalten Sie hier keinen realistischen Test.

Ein sinnvollerer Test wäre, die Lebensdauer Ihrer gesperrten Vorgänge zu verlängern, indem Sie eine kurze Verzögerung in das Schloss einfügen. Auf diese Weise sollten Sie wirklich in der Lage sein, die hinzugefügte Parallelität mit ReaderWriterLockSlimder durch basic implizierten Serialisierung zu vergleichen lock().

Derzeit geht die Zeit, die Ihre gesperrten Vorgänge benötigen, durch das Rauschen verloren, das durch die Sleep-Anrufe außerhalb der Sperren erzeugt wird. Die Gesamtzeit ist in beiden Fällen hauptsächlich schlafbezogen.

Sind Sie sicher, dass Ihre reale App die gleiche Anzahl an Lese- und Schreibvorgängen hat? ReaderWriterLockSlimist wirklich besser für den Fall, dass Sie viele Leser und relativ seltene Schriftsteller haben. 1 Writer-Thread im Vergleich zu 3 Reader-Threads sollten die ReaderWriterLockSlimVorteile besser demonstrieren , aber in jedem Fall sollte Ihr Test Ihrem erwarteten realen Zugriffsmuster entsprechen.

Steve Townsend
quelle
2

Ich denke, das liegt an dem Schlaf, den Sie in Ihren Leser- und Schreiber-Threads haben.
Ihr gelesener Thread hat einen Schlaf von 500 Tim und 50 ms, was 25000 entspricht. Die meiste Zeit schläft er

Itay Karo
quelle
0

Wann ist ReaderWriterLockSlim besser als ein einfaches Schloss?

Wenn Sie deutlich mehr Lese- als Schreibvorgänge haben.

Illidan
quelle