Kiedy ReaderWriterLockSlim jest lepszy niż zwykła blokada?

80

Robię bardzo głupi benchmark na ReaderWriterLock z tym kodem, w którym czytanie odbywa się 4x częściej niż pisanie:

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";
    }
}

Ale rozumiem, że oba działają mniej więcej w ten sam sposób:

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

Nawet jeśli czytamy 20 razy częściej niż pisze, wydajność jest nadal (prawie) taka sama.

Czy ja tu robię coś złego?

Z poważaniem.

vtortola
źródło
3
btw, jeśli wyjdę z uśpienia: „Zablokowany: 89 ms. RW Zablokowany: 32 ms.” - lub podbijając numery: „Zablokowany: 1871 ms. RW Zablokowany: 2506 ms.”.
Marc Gravell
1
Jeśli usunę uśpienia, zablokowane jest szybsze niż rwlocked
vtortola
1
Dzieje się tak, ponieważ kod, który synchronizujesz, jest zbyt szybki, aby wywołać jakąkolwiek rywalizację. Zobacz odpowiedź Hansa i moją edycję.
Dan Tao

Odpowiedzi:

107

W twoim przykładzie sen to oznacza generalnie nie ma sporu. Niezamknięty zamek jest bardzo szybki. Aby to miało znaczenie, potrzebowałbyś rywalizującego zamka; jeśli w tej rywalizacji są zapisy , powinny być mniej więcej takie same ( lockmogą być nawet szybsze) - ale jeśli są to głównie odczyty (rzadko z rywalizacją o zapis), spodziewałbym się, że ReaderWriterLockSlimblokada przewyższylock .

Osobiście wolę tutaj inną strategię, używając zamiany referencji - aby odczyty zawsze mogły czytać bez sprawdzania / blokowania / itp. Zapisy zmieniają się w sklonowanej kopii, a następnie używają Interlocked.CompareExchangedo zamiany odniesienia (ponowne zastosowanie ich zmiany, jeśli inny wątek zmutował odniesienie w międzyczasie).

Marc Gravell
źródło
1
Kopiuj i zamień przy zapisie jest zdecydowanie dobrym sposobem na to.
LBushkin
24
Nieblokujące kopiowanie i zamiana przy zapisie całkowicie pozwala uniknąć rywalizacji o zasoby lub blokowania czytelników; jest szybki dla pisarzy, gdy nie ma sporu, ale może zostać mocno zablokowany w obecności niezgody. Może być dobrze, aby pisarze nabyli blokadę przed wykonaniem kopii (czytelnicy nie przejmowaliby się blokadą) i zwolnili blokadę po wykonaniu zamiany. W przeciwnym razie, jeśli 100 wątków będzie próbować jednej jednoczesnej aktualizacji, w najgorszym przypadku będą musieli wykonać średnio około 50 operacji kopiowania-aktualizacji-try-wymiany (łącznie 5000 operacji kopiowania-aktualizacji-try-wymiany w celu wykonania 100 aktualizacji).
supercat
1
oto jak myślę, że to powinno wyglądać, proszę mi powiedzieć, czy źle: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 w zasadzie tak; w niektórych prostych przypadkach (zwykle podczas zawijania pojedynczej wartości lub gdy nie ma znaczenia, czy później nadpisze rzeczy, których nigdy nie widzieli) możesz nawet uciec od utraty Interlockedi po prostu użyć volatilepola i po prostu przypisać do niego - ale jeśli chcesz mieć pewność, że Twoja zmiana nie zgubiła się całkowicie, to Interlockedjest idealne
Marc Gravell
3
@ jnm2: Kolekcje często używają CompareExchange, ale nie sądzę, żeby dużo kopiowały. Blokada CAS + niekoniecznie jest zbędna, jeśli używa się jej, aby uwzględnić możliwość, że nie wszyscy pisarze uzyskają blokadę. Na przykład zasób, o który rywalizacja będzie na ogół rzadka, ale czasami może być poważna, może mieć blokadę i flagę wskazującą, czy twórcy powinni go nabyć. Jeśli flaga jest wyraźna, autorzy po prostu wykonują CAS, a jeśli się powiedzie, idą dalej. Jednak program zapisujący, który nie przejdzie pomyślnie CAS więcej niż dwukrotnie, może ustawić flagę; flaga mogłaby wtedy pozostać ustawiona ...
supercat
23

Moje własne testy wskazują, że ReaderWriterLockSlimma około 5 razy więcej narzutów w porównaniu do normalnego lock. Oznacza to, że aby RWLS działał lepiej niż zwykły stary zamek, ogólnie występowałyby następujące warunki.

  • Liczba czytelników znacznie przewyższa liczbę pisarzy.
  • Zamek musiałby być trzymany wystarczająco długo, aby pokonać dodatkowe obciążenie.

W większości rzeczywistych zastosowań te dwa warunki nie wystarczają do pokonania tego dodatkowego obciążenia. W szczególności w twoim kodzie zamki są utrzymywane przez tak krótki czas, że narzut zamka prawdopodobnie będzie dominującym czynnikiem. Gdybyś miał przenieść te Thread.Sleepwywołania do wnętrza zamka, prawdopodobnie uzyskałbyś inny wynik.

Brian Gideon
źródło
17

W tym programie nie ma sprzeczności. Metody Get i Add są wykonywane w ciągu kilku nanosekund. Szanse, że wiele wątków trafi w te metody w określonym czasie, są znikomo małe.

Umieść wywołanie Thread.Sleep (1) w nich i usuń uśpienie z wątków, aby zobaczyć różnicę.

Hans Passant
źródło
12

Edycja 2 : Po prostu usunąłem Thread.Sleeppołączenia z ReadThreadi WriteThread, widziałem Lockedlepsze wyniki RWLocked. Myślę, że Hans trafił tutaj w sedno; Twoje metody są zbyt szybkie i nie powodują sporu. Kiedy dodałem Thread.Sleep(1)do Geti Addmetody Lockedi RWLocked(i użyłem 4 wątków odczytu z 1 wątkiem zapisu), RWLockedpokonałem spodnie Locked.


Edycja : OK, gdybym naprawdę myślał, kiedy po raz pierwszy opublikowałem tę odpowiedź, zrozumiałbym przynajmniej, dlaczego umieściłeś tam Thread.Sleepwezwania: próbowałeś odtworzyć scenariusz, w którym odczyty odbywały się częściej niż pisze. To po prostu nie jest właściwy sposób. Zamiast tego wprowadziłbym dodatkowe obciążenie do twojegoAdd i Getmetod, aby stworzyć większą szansę na rywalizację (jak zasugerował Hans ), stworzyć więcej wątków do odczytu niż napisać wątki (aby zapewnić częstsze odczyty niż zapisy) i usunąć Thread.Sleepwywołania z ReadThreadi WriteThread(co w rzeczywistości zmniejszyć rywalizację, osiągając odwrotność tego, czego chcesz).


Podoba mi się to, co zrobiłeś do tej pory. Ale oto kilka problemów, które widzę od razu:

  1. Dlaczego Thread.Sleep dzwoni? To po prostu zawyżanie czasu wykonania o stałą wartość, co sztucznie spowoduje zbieżność wyników wydajności.
  2. Nie uwzględniłbym również tworzenia nowych Threadobiektów w kodzie, który jest mierzony przez Stopwatch. To nie jest trywialny obiekt do stworzenia.

Nie wiem, czy zauważysz znaczącą różnicę, gdy zajmiesz się dwoma powyższymi problemami. Uważam jednak, że należy się nimi zająć, zanim dyskusja będzie kontynuowana.

Dan Tao
źródło
+1: Dobra uwaga na # 2 - może naprawdę wypaczyć wyniki (taka sama różnica między każdym z czasów, ale różne proporcje). Chociaż jeśli będziesz biegać tak długo, czas tworzenia Threadobiektów zacznie tracić na znaczeniu.
Nelson Rothermel
10

Uzyskasz lepszą wydajność ReaderWriterLockSlimniż zwykła blokada, jeśli zablokujesz część kodu, która wymaga dłuższego czasu na wykonanie. W takim przypadku czytelnicy mogą pracować równolegle. Zdobycie a ReaderWriterLockSlimzajmuje więcej czasu niż wprowadzenie prostego Monitor. Sprawdź, czy moja ReaderWriterLockTinyimplementacja zawiera blokadę czytelnika-pisarza, która jest nawet szybsza niż prosta instrukcja blokady i oferuje funkcjonalność czytelnika-pisarza: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/

i255
źródło
Lubię to. W artykule zastanawiam się, czy Monitor jest szybszy niż ReaderWriterLockTiny na każdej platformie?
jnm2
Chociaż ReaderWriterLockTiny jest szybszy niż ReaderWriterLockSlim (a także Monitor), to ma jedną wadę: jeśli jest wielu czytelników, którzy zajmują trochę czasu, nigdy nie dał szans pisarzom (będą czekać prawie w nieskończoność) .ReaderWriterLockSlim z drugiej strony udaje się zrównoważyć dostęp do współdzielonych zasobów dla czytelników / pisarzy.
tigrou
3

Zdobycie niezakwestionowanych zamków zajmuje około mikrosekund, więc Twój czas wykonania będzie krótszy od wywołań Sleep.

MSN
źródło
3

Jeśli nie masz sprzętu wielordzeniowego (lub przynajmniej takiego samego, jak planowane środowisko produkcyjne), nie otrzymasz tutaj realistycznego testu.

Bardziej rozsądnym testem byłoby przedłużenie żywotności zablokowanych operacji poprzez wprowadzenie krótkiego opóźnienia wewnątrz zamka. W ten sposób naprawdę powinieneś być w stanie porównać równoległość dodaną przy użyciu ReaderWriterLockSlimz serializacją implikowaną przez basiclock() .

Obecnie czas potrzebny na zablokowane operacje jest tracony w hałasie generowanym przez wywołania uśpienia, które mają miejsce poza zamkami. Całkowity czas w obu przypadkach jest głównie związany ze snem.

Czy na pewno Twoja aplikacja w świecie rzeczywistym będzie miała taką samą liczbę odczytów i zapisów? ReaderWriterLockSlimjest naprawdę lepsza w przypadku, gdy masz wielu czytelników i stosunkowo rzadkich pisarzy. 1 wątek pisarza w porównaniu z 3 wątkami czytającymi powinny ReaderWriterLockSlimlepiej pokazywać korzyści, ale w każdym przypadku test powinien pasować do oczekiwanego wzorca dostępu w świecie rzeczywistym.

Steve Townsend
źródło
2

Wydaje mi się, że jest to spowodowane snami, które masz w swoich wątkach czytelnika i pisarza.
Twój wątek do czytania ma 500 razy 50 ms snu, który wynosi 25000 Przez większość czasu śpi

Itay Karo
źródło
0

Kiedy ReaderWriterLockSlim jest lepszy niż zwykła blokada?

Kiedy masz znacznie więcej odczytów niż zapisów.

Illidan
źródło