Laden...

Tipp: Besser skalieren mit ReaderWriterGate

Erstellt von svenson vor 17 Jahren Letzter Beitrag vor 17 Jahren 1.622 Views
S
svenson Themenstarter:in
8.746 Beiträge seit 2005
vor 17 Jahren
Tipp: Besser skalieren mit ReaderWriterGate

Wer mit der Skalierbarkeit von Leser-/Schreiber-Zugriffsmustern in Server-Anwendungen zu kämpfen hat, wird diesen Artikel mit Gold aufwiegen wollen:

http://msdn.microsoft.com/msdnmag/issues/06/11/ConcurrentAffairs/default.aspx?loc=de

Vorteil gegenüber dem klassischen Verfahren (ReaderWriterLock) ist das optimierte Threading mittels Queues.

T
512 Beiträge seit 2006
vor 17 Jahren

Also wenn ich das richtig überschaue, erreicht man den selben Effekt auch deutlich einfacher.

Man braucht nur ein AutoResetEvent Objekt und die ThreadPool Funktion RegisterWaitForSingleObject.

Alle Operationen werden dann über das AutoResetEvent synchronisiert. Das erreicht man, indem man die Funktionen dann über RegisterWaitForSingleObject statt mit QueueUserWorkItem im Threadpool registriert.

Das würde schon ausreichen, um den Threadpool während einer Schreiboperation nicht mit wartenden Threads vollzumüllen. Die Queue würde dann vom ThreadPool übernommen. Das würde z.B. so aussehen:

AutoResetEvent waitObject = new AutoResetEvent(true);
// true damit erste Operation sofort ausgeführt wird

void RuftSchreibenAuf()
{
    ThreadPool.RegisterWaitForSingleObject( waitObject, WriteCallback, null, Timeout.Infinite, true );
}

void RuftLesenAuf()
{
    ThreadPool.RegisterWaitForSingleObject( waitObject, ReadCallback, null, Timeout.Infinite, true );
}

void WriteCallback( object state, bool timedOut )
{
    try {
        // hier findet das echte Schreiben statt
    }
    finally {
        waitObject.Set(); // gibt für die nächste Operation frei
    }
}

void ReadCallback( object state, bool timedOut )
{
    waitObject.Set(); // gibt die nächste Operation sofort frei

    // Lesen findet dann hier statt
    // weil vor dem eigentlichen Lesen schon freigegeben wird,
    // könnten auch mehrere Leseoperation parallel stattfinden
}

Damit gibt es noch ein Problem mit der anderen Richtung. Wenn gerade ein paar Threads fleißig am Lesen sind, und einer will was Schreiben, dann wird das nicht verhindert. Er kann einfach anfangen obwohl die Leser noch nicht fertig sind.

Das könnte man zum Beispiel mit dem ReaderWriterLock in den Griff bekommen. Im Ganzen hätte man dann maximal einen Schreibethread im Wartezustand, solange noch Leseoperationen laufen, die vorher aufgerufen wurden. Leseoperationen die nach einer Schreibeoperation aufgerufen werden, würden vom ThreadPool gequeued werden und würden keinen Thread belegen, bis der Schreiber fertig ist.

Im Ganzen würde der Code dann zum Beispiel so aussehen:

AutoResetEvent waitObject = new AutoResetEvent(true);
// true damit erste Operation sofort ausgeführt wird
ReadWriterLock readWriterLock = new ReadWriterLock();

void RuftSchreibenAuf()
{
    ThreadPool.RegisterWaitForSingleObject( waitObject, WriteCallback, null, Timeout.Infinite, true );
}

void RuftLesenAuf()
{
    ThreadPool.RegisterWaitForSingleObject( waitObject, ReadCallback, null, Timeout.Infinite, true );
}

void WriteCallback( object state, bool timedOut )
{
    readWriterLock.AcquireWriterLock( Timeout.Infinite );
    try {
        // hier findet das echte Schreiben statt
    }
    finally {
        readWriterLock.ReleaseWriterLock();
        waitObject.Set(); // gibt für die nächste Operation frei
    }
}

void ReadCallback( object state, bool timedOut )
{
    readWriterLock.AcquireReaderLock( Timeout.Infinite );
    try {
        waitObject.Set(); // gibt die nächste Operation sofort frei

        // Lesen findet dann hier statt
    }
    finally {
        readWriterLock.ReleaseReaderLock();
    }
}

Nicht wirklich sonderlich umständlich finde ich. Und den Aufwand kann man sich sparen, wenn man paralleles Lesen nicht braucht, da würde Variante 1 auch reichen, wenn man das Lesen genauso strickt wie das Schreiben (also Set() erst am Ende aufrufen). Dann würde immer nur eine Operation gleichzeitig laufen, der Rest würde über den ThreadPool gequeued werden. Ich glaube sowieso nicht so richtig, dass paralleles Lesen etwas bringen würde. Selbst mit 4 Prozessoren teilen die sich doch immernoch die gleiche Schnittstelle um auf die Festplatte zuzugreifen.

Das ist zumindest was mir in den ersten 10 Minuten dazu einfällt. Mich würde da mal eure Kritik interessieren. Scheint mir vom Ergebniss her aber ziemlich analog zu funktionieren. Man könnte bestimmt noch was verbessern, und vieleicht ists auch völliger Blödsinn.

e.f.q.

Aus Falschem folgt Beliebiges

S
svenson Themenstarter:in
8.746 Beiträge seit 2005
vor 17 Jahren

Bei deinem Code wird der Writer nicht priorisiert im Threadpool behandelt, wenn weitere Schreibanforderungen während einer bereits laufenden reinkommen. Bei dir bestimmt die Reihenfolge der Anfrage die Abarbeitung. Daher die händische Listenverwaltung.

Wenn man davon ausgeht, dass Lese-Anfragen aber möglichst up-to-date beantwortet werden sollen, dann ist das wünschenswert. Ist das nicht so wichtig, dann ist deine Lösung sicher um einiges schlanker.

T
512 Beiträge seit 2006
vor 17 Jahren

Danke für die Klarstellung, das war der Teil den ich übersehen habe. Das macht es natürlich umständlicher und eine eigene Queue notwendig.

e.f.q.

Aus Falschem folgt Beliebiges