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.
c#
.net
multithreading
locking
vtortola
źródło
źródło
Odpowiedzi:
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 (
lock
mogą być nawet szybsze) - ale jeśli są to głównie odczyty (rzadko z rywalizacją o zapis), spodziewałbym się, żeReaderWriterLockSlim
blokada 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.CompareExchange
do zamiany odniesienia (ponowne zastosowanie ich zmiany, jeśli inny wątek zmutował odniesienie w międzyczasie).źródło
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; }
Interlocked
i po prostu użyćvolatile
pola i po prostu przypisać do niego - ale jeśli chcesz mieć pewność, że Twoja zmiana nie zgubiła się całkowicie, toInterlocked
jest idealneMoje własne testy wskazują, że
ReaderWriterLockSlim
ma około 5 razy więcej narzutów w porównaniu do normalnegolock
. Oznacza to, że aby RWLS działał lepiej niż zwykły stary zamek, ogólnie występowałyby następujące warunki.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.Sleep
wywołania do wnętrza zamka, prawdopodobnie uzyskałbyś inny wynik.źródło
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ę.
źródło
Edycja 2 : Po prostu usunąłem
Thread.Sleep
połączenia zReadThread
iWriteThread
, widziałemLocked
lepsze wynikiRWLocked
. Myślę, że Hans trafił tutaj w sedno; Twoje metody są zbyt szybkie i nie powodują sporu. Kiedy dodałemThread.Sleep(1)
doGet
iAdd
metodyLocked
iRWLocked
(i użyłem 4 wątków odczytu z 1 wątkiem zapisu),RWLocked
pokonałem spodnieLocked
.Edycja : OK, gdybym naprawdę myślał, kiedy po raz pierwszy opublikowałem tę odpowiedź, zrozumiałbym przynajmniej, dlaczego umieściłeś tam
Thread.Sleep
wezwania: 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
iGet
metod, 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.Sleep
wywołania zReadThread
iWriteThread
(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:
Thread.Sleep
dzwoni? To po prostu zawyżanie czasu wykonania o stałą wartość, co sztucznie spowoduje zbieżność wyników wydajności.Thread
obiektów w kodzie, który jest mierzony przezStopwatch
. 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.
źródło
Thread
obiektów zacznie tracić na znaczeniu.Uzyskasz lepszą wydajność
ReaderWriterLockSlim
niż 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 aReaderWriterLockSlim
zajmuje więcej czasu niż wprowadzenie prostegoMonitor
. Sprawdź, czy mojaReaderWriterLockTiny
implementacja 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/źródło
Przeczytaj ten artykuł: http://blogs.msdn.com/b/pedram/archive/2007/10/07/a-performance-comparison-of-readerwriterlockslim-with-readerwriterlock.aspx
Twoje snu są prawdopodobnie na tyle długie, że blokowanie / odblokowywanie jest statystycznie nieistotne.
źródło
Zdobycie niezakwestionowanych zamków zajmuje około mikrosekund, więc Twój czas wykonania będzie krótszy od wywołań
Sleep
.źródło
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
ReaderWriterLockSlim
z 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?
ReaderWriterLockSlim
jest 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 powinnyReaderWriterLockSlim
lepiej pokazywać korzyści, ale w każdym przypadku test powinien pasować do oczekiwanego wzorca dostępu w świecie rzeczywistym.źródło
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
źródło
Kiedy masz znacznie więcej odczytów niż zapisów.
źródło