Concurrent Affairs
Die Sperre ReaderWriterGate
Jeffrey Richter

Inhalt
Als ich eines Tages an einem Beratungsprojekt arbeitete, stieß ich auf ein Problem bei der Threadsynchronisierung, das mir bislang vollkommen neu war. Die Firma war gerade dabei, einen Webdienst aufzubauen, und fast alle der auf Ihrem Server eingehenden Kundenanfragen erforderten den Zugriff auf freigegebene Daten im schreibgeschützten Modus. Gelegentlich ging eine Anfrage ein, die es eigentlich erforderlich machte, freigegebene Daten zu ändern, doch mussten die Daten synchronisiert bleiben. Es klang wie das perfekte Szenario für den Einsatz der so genannten Leser-Schreiber-Sperren (beschrieben in meinem
Concurrent Affairs-Artikel vom Juni 2006, möglicherweise in englischer Sprache).
Nachdem ich jedoch noch intensiver in das Projekt eingestiegen war, erkannte ich bald, dass die Anforderungen dieses Unternehmens eine ungewöhnliche Schwierigkeit aufwiesen: Wenn eine Webdienst-Anfrage beim Server einging und der Schreibschutz aktiviert wurde, hielt diese Sperre extrem lange an. Während die freigegebenen Daten über den Schreibthread modifiziert wurden, gingen andere Webdienst-Anfragen mit Leseanforderungen beim Server ein. Hier bestand das Problem darin, dass die Threads im Pool zwar zunächst aktiviert und die Leseanforderungen bearbeitet wurden, jedoch alle Threads umgehend wieder deaktiviert wurden und warteten, bis die Aufgabe des Schreibthreads abgeschlossen worden war.
Es dauerte nicht lange, bis Hunderte von Threads im Pool erstellt worden waren, und alle dieser Threads befanden sich innerhalb des Betriebssystem-Kernels in einem Wartezustand. Wir hatten jetzt jede Menge Threads, die nicht aktiviert werden konnten und stattdessen in der Warteschleife wertvolle Ressourcen verbrauchten (wie z. B. jeder Stapel des Threads samt dem dazugehörigen Threadkernelobjekt). Die Situation war ungünstig, wurde aber noch schlimmer. Als der Schreibthread den Schreibzugriff auf die freigegebenen Daten abgeschlossen hatte, wurde die Sperre aufgehoben. Die Sperre erkannte dann, dass Hunderte von Lesethreads auf die Aufhebung der Sperre warteten, woraufhin alle Lesethreads in der Warteschlange eine Freigabe erhielten. Der Server verfügte über vier CPUs, und diese vier kleinen CPUs mussten nun die gesamte Flut auszuführender Threads verarbeiten. Windows® würde die Verarbeitungsreihenfolge aller Threads mithilfe seiner gängigen Methode der Round-Robin-Zeitplanung festlegen, doch der Aufwand, der mit dem Umschalten der CPUs von einem Thread zum nächsten verbunden war, wirkte sich negativ auf die Leistung, Skalierbarkeit und den Durchsatz aus.
Monatelang beschäftigte mich dieses Problem, bis ich schließlich meine eigene Threadsynchronisierungssperre entwickelte, die ich als ReaderWriterGate bezeichne. Diese Sperre ist schnell, und sie passt sehr gut zu einer ganzen Reihe von Anwendungen. Das Interessante an ReaderWriterGate ist, dass das Aufrufen von Threads fast nie blockiert wird, und wenn doch, dann ist die Blockierung garantiert nur von sehr kurzer Dauer. Und aufgrund der Tatsache, dass sich ReaderWriterGate einer meiner von ResourceLock abgeleiteten Klassen bedient (Näheres hierzu finden Sie im oben erwähnten Juni-Artikel), musste ich nur eine einzige Codezeile ändern, um die interne Verwendung eines Spinlock durch ReaderWriterGate zu programmieren, so dass Threads innerhalb des Windows-Kernels niemals blockiert werden (Näheres hierzu finden Sie in meinem
Concurrent Affairs-Artikel vom Oktober 2005, möglicherweise in englischer Sprache). Dies sollte zu einer noch besseren Leistung führen, doch muss ich ehrlicherweise zugeben, dass bei dieser Lösung alle Threads, die ReaderWriterGate nutzen, dieselbe Threadpriorität haben müssen und dass die Prioritätssteigerung deaktiviert sein muss. Dies hätte dann eine noch höhere Leistung zur Folge. Darüber hinaus kann ReaderWriterGate ein außergewöhnlich hohes Arbeitsaufkommen mit nur wenigen Threads bewältigen. Somit werden nur sehr wenige Ressourcen benötigt, unabhängig von der Anzahl der Anfragen, die bei dem Gate ankommen.
Ich habe meine eigene Version dieses Primitivs implementiert, die ich kostenlos im Rahmen der Power Threading Library unter
www.wintellect.com zum Download bereitstelle. Wenn Sie meine Implementierung nutzen möchten, können Sie dies gemäß dem Endnutzer-Lizenzvertrags (EULA) tun, den Sie ebenfalls in der Library finden. Weitere Informationen entnehmen Sie bitte dem Endbenutzer-Lizenzvertrag (EULA).
Im Folgenden wird näher erläutert, worauf ReaderWriterGate basiert und welche Implementierung möglich ist. Generell lernen Sie eine neue Denkweise kennen, was das Threading und die Threadsynchronisierung angeht. Vielleicht erkennen Sie Bereiche in Ihrem bestehenden Code, in denen diese Denkweise angewendet werden kann. Einige Ideen können bereits bei minimaler Veränderung Ihrer Architektur umgesetzt werden und somit die Leistungsfähigkeit und Skalierbarkeit Ihrer Anwendungen erhöhen.
Das ReaderWriterGate-Objektmodell
Um die ReaderWriterGate-Klasse anwenden zu können, müssen Sie sich lediglich mit ein paar Methoden vertraut machen:
public sealed class ReaderWriterGate
{
public ReaderWriterGate();
public void QueueWrite(
ReaderWriterGateCallback callback, Object state);
public void QueueRead(
ReaderWriterGateCallback callback, Object state);
... // Some methods not shown to simplify the discussion
}
Wie Sie sehen, handelt es sich hierbei um ein sehr einfaches Objektmodell (einfacher als die meisten Sperrfunktionen). Wenn Sie eine Aufgabe durchführen möchten, für die ein schreibgeschützter/gemeinsamer Zugriff erforderlich ist, rufen Sie einfach die QueueRead-Methode auf. Möchten Sie hingegen eine Aufgabe durchführen, für die Sie einen schreibberechtigten/exklusiven Zugriff auf eine Ressource benötigen, rufen Sie bitte die QueueWrite-Methode auf. Beiden Methoden verlangen zwei Argumente. Das erste Argument, eine Instanz von ReaderWriterGateCallback, identifiziert eine von Ihnen festgelegte Methode, und diese Methode führt wiederum das eigentliche Lesen oder Schreiben aus. Bei ReaderWriterGateCallback handelt es sich um einen Delegattyp, der wie folgt definiert wird:
delegate void ReaderWriterGateCallback(
ReaderWriterGateReleaser releaser);
Das zweite Argument, das an QueueRead und QueueWrite übergeben wird, ist ein Verweis auf ein beliebiges Objekt. Dieser Verweis wird nun an Ihre Callback-Methode übergeben, die das eigentliche Lesen oder Schreiben ausführt. Dadurch erhalten Sie einfach die Möglichkeit, einige Daten von der Methode zu erhalten, die die Ausführung der Aufgabe anfordert, und diese Daten an den Thread oder die Methode weiterzugeben, der bzw. die die eigentliche Aufgabe ausführt. Wenn Sie mit dem .NET-ThreadPool und dessen QueueUserWorkItem-Methode vertraut sind, kommt Ihnen dieses Modell vermutlich ein wenig bekannt vor.
Beachten Sie, dass es der ReaderWriterGateCallback-Delegat erforderlich macht, dass Ihre Callback-Methode nur einen Parameter des Typs ReaderWriterGateReleaser verlangt. Nachstehend wird veranschaulicht, wie dieser Typ aussieht:
public sealed class ReaderWriterGateReleaser : IDisposable
{
// Returns second argument passed to QueueXxx
public object State { get; }
// Returns reference to Gate
public ReaderWriterGate Gate { get; }
public void Release(); // Calls Dispose internally
public void Dispose(); // Useful with C#'s using statement
}
Im Rahmen Ihrer Callback-Methode können Sie die State-Eigenschaft abfragen, um den Objektverweis abzurufen, der als zweites Argument an die QueueRead/QueueWrite-Methoden übergeben wurde. Mit Ihrer Callback-Methode können Sie auch die Gate-Eigenschaft abfragen, um einen Verweis auf ReaderWriterGate zu erhalten, mit dem der Aufruf der Methode abgestimmt wurde. Es ist unwahrscheinlich, dass Ihre Methode diese Eigenschaft jemals abfragt, es sei denn, sie initiiert eine andere Aufgabe auf derselben Ressource (geschützt durch das gleiche ReaderWriterGate-Objekt).
Der grundlegende Zweck des ReaderWriterGateReleaser-Objekts besteht darin, dass über Ihre Callback-Methode angegeben werden kann, dass das Aufrechthalten des Ressourcen-Gates während der anhaltenden Ausführung beendet wurde. Als Nächstes sehen Sie ein Beispiel, das aufzeigt, inwiefern diese Methode auch für Sie nützlich sein könnte und wie sie richtig eingesetzt wird. Eigentlich vermute ich, dass die meisten Callback-Methoden das ReaderWriterGateReleaser-Argument komplett ignorieren. In diesem Fall wird das Gate automatisch freigegeben, sobald Ihre Callback-Methode zurückgegeben wird.
Verwenden von ReaderWriterGate
Angenommen, Sie richten einen Webdienst für Katalogbestellungen von Kunden ein. Darüber hinaus nimmt der Dienst noch Anfragen von Mitarbeitern entgegen, die Aktualisierungen in den Katalog einbringen möchten. Wenn der Dienst initialisiert wird, erstellt er eine Instanz einer hypothetischen CatalogOrderSystem-Klasse (siehe Abbildung 1). Wird ein CatalogOrderSystem-Objekt erstellt, initialisiert es ein privates Feld, mit dem es auf ein ReaderWriterGate-Objekt verweist. Dieses Objekt stimmt Threads miteinander ab, die auf dieses CatalogOrderSystem-Objekt Lese- und Schreibzugriff haben.

Figure 1 CatalogOrderSystem-Klasse
internal sealed class CatalogOrderSystem
{
// The gate used to protect access to the Catalog Order System’s data
private ReaderWriterGate m_gate = new ReaderWriterGate();
public void UpdateCatalogWS(CatalogEntry[] catalogEntries)
{
... // Perform any validation/pre-processing on catalogEntries...
// Updating the catalog requires exclusive access to it.
m_gate.QueueWrite(UpdateCatalog, catalogEntries);
}
// The code in this method has exclusive access to the catalog.
private void UpdateCatalog(ReaderWriterGateReleaser r)
{
CatalogEntry[] catalogEntries = (CatalogEntry[]) r.State;
... // Update the catalog with the new entries...
} // When this method returns, exclusive access is relinquished.
public void BuyCatalogProductsWS(CatIDAndQuantity[] items)
{
// Buying products requires read access to the catalog.
m_gate.QueueRead(BuyCatalogProducts, items);
}
// The code in this method has shared read access to the catalog.
private void BuyCatalogProducts(ReaderWriterGateReleaser r)
{
using (r)
{
CatIDAndQuantity[] items = (CatIDAndQuantity[])r.State;
foreach (CatIDAndQuantity item in items)
{
... // Process each catalog item to build customer’s order...
}
} // When r is Disposed, read access is relinquished.
... // Save customer’s order to database
... // Send customer e-mail confirming order
}
}
Angenommen, eine Anfrage bezüglich der Aktualisierung des Katalogs geht ein. In diesem Fall wird die UpdateCatalogWS-Methode aufgerufen und ein Verweis an ein Array von CatalogEntry-Objekten übergeben. Innerhalb von UpdateCatalogWS werden zunächst die für die Objekte des Arrays erforderlichen Validierungen und Vorverarbeitungen vorgenommen. Diese Arbeit wird ohne Abruf von ReaderWriterGate ausgeführt, so dass dieser Thread parallel mit anderen Threads arbeiten kann, die zur gleichen Zeit auf das CatalogOrderSystem-Objekt zugreifen. Wenn alles korrekt verläuft, wird zur eigentlichen Änderung des Bestellkatalogs durch neue Einträge jetzt noch der exklusive Zugriff auf die Katalogbestelldaten benötigt. Also wird QueueWrite aufgerufen und UpdateCatalog (die Methode, die den Katalog letztendlich aktualisiert) sowie das Array mit neuen Katalogeinträgen übergeben.
Intern lässt ReaderWriterGate ausschließlich dann einen Thread aus dem Threadpool UpdateCatalog aufrufen, wenn der exklusive Zugriff dem Code gewährt werden kann, der Rahmen dieser Methode ausgeführt wird. Wird UpdateCatalog aufgerufen, kann davon ausgegangen werden, dass gerade keine anderen Threads in dem OrderCatalogSystem-Objekt lesen oder schreiben und dass die Katalogeinträge nach Bedarf aktualisiert oder geändert werden können. Sobald die UpdateCatalog-Methode zurückgegeben wird, wird das Gate automatisch freigegeben, so dass jede in der Warteschlange befindliche Methode aufgerufen werden und durch ihren Code das OrderCatalogSystem-Objekt bearbeiten kann.
Nun wird davon ausgegangen, dass eine Anfrage mit einer Bestellungsanfrage eingeht. Dies bedeutet, dass die BuyCatalogProductsWS-Methode aufgerufen wird und dass diese Methode einen Verweis an ein Array von CatIDAndQuantity-Objekten übergibt. Innerhalb von BuyCatalogProductsWS muss sie die Kataloginformationen lesen, um die Bestellanfrage des Kunden zu bearbeiten. Um sicherzustellen, dass der Katalog sicher gelesen werden kann, ist eine Leseberechtigung erforderlich. Somit wird QueueRead neben BuyCatalogProducts (der Methode, die letztendlich die Bestellung des Kunden ausführt) und dem Array an Artikeln aufgerufen.
ReaderWriterGate ruft ausschließlich dann BuyCatalogProducts auf, wenn der Lesezugriff dem Code erteilt werden kann, der im Rahmen dieser Methode ausgeführt wird. Wird BuyCatalogProducts aufgerufen, kann davon ausgegangen werden, dass keine anderen Threads gerade in dem OrderCatalogSystem-Objekt schreiben (auch wenn andere Threads eventuell gerade darin lesen) und dass die Aufgaben nach Bedarf ausgeführt werden können. Sobald die BuyCatalogProducts-Methode ausgeführt wird, werden zunächst die Katalogeinträge im Katalog nachgeschlagen. Hier ist Lesezugriff auf den Katalog erforderlich. Sobald alle Einträge nachgeschlagen wurden, ist kein weiterer Zugriff auf den Katalog mehr erforderlich. Allerdings steht jetzt noch mehr Arbeit an, damit die Bearbeitung der Kundenbestellung abgeschlossen werden kann. Normalerweise wird ReaderWriterGate freigegeben, sobald die Callback-Methode zurückgegeben wird. Doch um die Leistung und Skalierbarkeit zu verbessern, versucht die BuyCatalogProducts-Methode, das Gate so früh wie möglich freizugeben, d. h. sobald kein Code mehr ausgeführt wird und kein Lesezugriff auf den Katalog mehr besteht.
Um das Gate freizugeben, bevor die Methode zurückgegeben wird, habe ich eine C#-basierende Anweisung eingesetzt. Bei der abschließenden Klammer dieser Anweisung gibt der C#-Compiler einen finally-Block frei, der Code enthält, mit dem die Methode „ReaderWriterGateReleaser's Dispose“ aufgerufen werden kann. Diese benachrichtigt ReaderWriterGate, dass der Lesezugriff des Threads auf die Ressource beendet ist und die Sperre zulassen kann, dass andere in der Warteschlange befindliche Methoden aufgerufen werden. An diesem Punkt wird die BuyCatalogProducts-Methode noch nicht zurückgegeben, sondern fährt statt dessen mit der Verarbeitung der Kundenanfrage fort. Sobald die BuyCatalogProducts-Methode zurückgegeben, wird ReaderWriterGate ganz normal freigegeben. In diesem Fall weiß jedoch der Code innerhalb des ReaderWriterGates, der BuyCatalogProducts aufgerufen hat, dass ReaderWriterGate bereits freigegeben ist und nicht erneut freigegeben wird. Gemäß der .NET Framework-Richtlinien für die Implementierung von IDisposable gibt tatsächlich nur der erste Aufruf das Gate frei, wenn Sie Dispose (oder Release) auf einem einzelnen ReaderWriterGateReleaser-Objekt mehrere Male aufrufen. Alle weiteren Aufrufe bewirken nichts und werden einfach zurückgegeben.
ReaderWriterGate: Die Implementierung
Jetzt, da Sie wissen, wie Sie die ReaderWriterGate-Klasse einsetzen können, werden Sie erfahren, wie ich sie implementiert habe. Ein ReaderWriterGate-Objekt beinhaltet die privaten Instanzfelder, die in Abbildung 2 aufgezeigt sind.

Figure 2 Private Instanzfelder von ReaderWriterGate
public sealed class ReaderWriterGate
{
// Used for thread-safe access to other fields
private ResourceLock m_syncLock = new MonitorResourceLock();
// The current state of the gate; see the ReaderWriteGateStates enum
private ReaderWriterGateStates m_state = ReaderWriterGateStates.Free;
// The number of methods desiring shared access currently executing
private Int32 m_numReaders = 0;
// A FIFO queue of methods desiring exclusive access
private Queue<ReaderWriterGateReleaser> m_qWriteRequests =
new Queue<ReaderWriterGateReleaser>();
// A FIFO queue of methods desiring shared access
private Queue<ReaderWriterGateReleaser> m_qReadRequests =
new Queue<ReaderWriterGateReleaser>();
}
private enum ReaderWriterGateStates
{
// No methods are desiring access
Free,
// One or more methods desiring shared access are executing
// and no writer methods are queued up desiring access
OwnedByReaders,
// One or more methods desiring shared access are executing
// and one or more writer methods are queued up desiring access
OwnedByReadersAndWriterPending,
// One method desiring exclusive access is executing
OwnedByWriter
}
Wenn ein Thread QueueWrite aufruft, erstellt diese Methode sofort ein ReaderWriterGateReleaser-Objekt und übergibt dem Konstruktor den Delegat der Methode, der in die Ressource schreibt. Außerdem übergibt die Methode einen Verweis auf das ReaderWriterGate-Objekt und einen booleschen Wert true darüber, dass die Callback-Methode in die Ressource schreibt, sowie das State-Objekt, das als zweites Argument an QueueWrite übergeben wird. Der Thread, der QueueWrite aufruft, prüft das Feld m_state und legt die weitere Vorgehensweise fest. Abbildung 3 zeigt die grundlegende Logik, die innerhalb von QueueWrite umgesetzt wird.

Figure 3 Grundlegende Logik von QueueWrite
1. If m_state is Free
1.1 Set m_state to OwnedByWriter
1.2 Invoke callback method via ThreadPool’s QueueUserWorkItem
1.3 Return to caller
2. If m_state is OwnedByReaders or OwnedByReadersAndWriterPending
2.1 Set m_state to OwnedByReadersAndWriterPending
2.2 Add ReaderWriterGateReleaser object to m_qWriteRequests
2.3 Return to caller
3. If m_state is OwnedByWriter
3.1 Do not change m_state
3.2 Add ReaderWriterGateReleaser object to m_qWriteRequests
3.3 Return to caller
Bitte beachten Sie, dass QueueWrite immer sofort wieder zurückgegeben wird und nie darauf wartet, dass die Callback-Methode ihre Ausführung beendet. Dieses Verhalten ist anders als bei normalen Threadsynchronisierungssperren, wie z. B. Monitor oder ReaderWriterLock, bei denen der aufrufende Thread so lange blockiert wird, bis ihm der Zugriff gewährt wird. Für ReaderWriterGate lässt der aufrufende Thread die Callback-Methode entweder über einen Thread des Threadpools ausführen oder er fügt einer Warteschlange einen Eintrag hinzu. Der aufrufende Thread wartet niemals darauf, Zugriff auf die durch das Gate geschützte Ressource zu erhalten. Dies ist ein äußerst wichtiges Unterscheidungsmerkmal. Der Thread, der QueueWrite aufruft, kann also nicht sicher sein, dass die Callback-Methode zum Zeitpunkt der Rückgabe von QueueWrite ausgeführt worden ist.
Wenn ein Thread QueueRead aufruft, erstellt diese Methode sofort ein ReaderWriterGateReleaser-Objekt und übergibt dem Konstruktor den Delegat der Methode, der die Ressource liest. Außerdem übergibt sie einen Verweis auf das ReaderWriterGate-Objekt und einen booleschen Wert false darüber, dass die Callback-Methode die Ressource liest, sowie das State-Objekt, das als zweites Argument an QueueRead übergeben wurde. Abbildung 4 zeigt die grundlegende Logik, die innerhalb von QueueRead umgesetzt wird.

Figure 4 Pseudocode für QueueRead
1. If m_state is Free or OwnedByReaders
1.1 Set m_state to OwnedByReaders
1.2 Add 1 to m_numReaders
1.3 Invoke callback method via ThreadPool’s QueueUserWorkItem
1.4 Return to caller
2. If m_state is OwnedByWriter or OwnedByReadersAndWriterPending
2.1 Do not change m_state
2.2 Add ReaderWriterGateReleaser object to m_qReadRequests
2.3 Return to caller
Ebenso wie QueueWrite wird QueueRead immer sofort zurückgegeben und wartet niemals darauf, dass die Callback-Methode ihre Aufgabe vollständig durchgeführt hat. Wird QueueRead aufgerufen, reiht diese Funktion entweder die Callback-Methode in die Warteschlange des Threadpools ein oder fügt der Warteschlange m_qReadRequests das ReaderWriterGateReleaser-Objekt hinzu, bevor die Methode wieder deaktiviert wird. Der Thread, der QueueRead aufruft, kann also nicht sicher sein, dass die Callback-Methode zum Zeitpunkt der Rückgabe von QueueRead ausgeführt worden ist.
Die Vorteile von ReaderWriterGate
Sehen Sie sich noch einmal das Webdienst-Beispiel zurück. Angenommen, ein Thread fordert Schreibzugriff beim Server an. Zunächst wird ein Thread des Threadpools aktiviert, der QueueWrite aufruft. Ist das Gate frei, reiht sich die Callback-Methode in die Warteschlange des Threadpools ein, und die QueueWrite-Methode wird umgehend zurückgegeben. Der Thread des Threadpools wird wahrscheinlich wieder in den Threadpool zurückgeführt. Wird er zum Threadpool zurückgeführt, erkennt er die in der Warteschlange befindliche Callback-Methode und fordert die Methode auf, die Aufgabe auszuführen. In diesem Fall erledigt ein Thread alles, und es ist keinerlei Kontextwechsel erforderlich – die Leistung ist fantastisch!
Angenommen, die Callback-Methode nimmt viel Zeit für die Ausführung in Anspruch. Während ihrer Ausführung könnten zusätzliche Anfragen im Webserver eingehen. In diesem Beispiel gehen 100 Leseanfragen ein. Alle diese Anfragen werden in der Warteschlange des Threadpools der Common Language Runtime (CLR) eingereiht. Da ein Thread des Threadpools damit beschäftigt ist, die vorher in die Warteschlange eingereihte Write-Methode auszuführen, wird ein anderer Thread des Threadpools aktiviert, der den Webdienst-Code aufruft. Der Webdienst-Code ruft dann wiederum QueueRead auf, erkennt, dass das Gate nicht frei ist, und erstellt daher lediglich ein ReaderWriterGateReleaser-Objekt, das den Rückruf identifiziert, und fügt es der Warteschlange m_qReadRequests ein. Dieser zweite Thread des Threadpools wird dann zunächst von QueueRead und schließlich auch an den Threadpool zurückgegeben.
Beachten Sie, dass dieser Thread nicht blockiert ist. Wenn 99 weitere Leseanforderungen bei dem Webdienst eingehen, extrahiert dieser Thread tatsächlich die 99 Leseanforderungen (nacheinander) aus dem Threadpool und fügt der Warteschlange m_qReadRequests einfach eine Reihe ReaderWriterGateReleaser-Objekte hinzu. Bis zu diesem Punkt hat der Threadpool nur zwei Threads benötigt. Ein Thread ist mittlerweile wieder zurück im Pool und bereit, weitere eingehende Anfragen zu bearbeiten, während der andere Thread immer noch die Write-Methode ausführt. Wenn eine Schreibanforderung eingeht, wird ein ReaderWriterGateReleaser-Objekt erstellt und dann in die Warteschlange m_qWriteRequests eingereiht, und der Thread wird wieder sofort an den Threadpool zurückgegeben. Dies ist eine außerordentlich gut skalierbare Lösung, die nur eine extrem kleine Anzahl System-Ressourcen in Anspruch nimmt.
Jetzt, da der erste Threadpool-Thread die Verarbeitung der Write-Methode beendet, wird das Gate automatisch freigegeben und ReaderWriterGate muss dann seine Warteschlangen untersuchen, um festzustellen, welche Write- oder Read-Methoden als Nächstes aufzurufen sind. Die interne Logik sieht ähnlich aus wie in Abbildung 5 gezeigt.

Figure 5 Pseudocode für das Prüfen von Warteschlangen
1. If a reader thread is releasing the gate, do steps 1.1 & 1.2;
else, go to step #2
1.1. Subtract 1 from m_numReaders
1.2. If m_numReaders is > 0, return to caller; else, goto step #2
2. If m_qWriteRequests.Count > 0
2.1. Set m_state to OwnedByWriter
2.2. Invoke 1 writer callback method via ThreadPool’s
QueueUserWorkItem
2.3. Return to caller
3. If m_qReadRequests.Count > 0
3.1. Set m_state to OwnedByReaders
3.2. Set m_numReaders = m_qReadRequests.Count
3.3. Invoke all reader callback methods via ThreadPool’s
QueueUserWorkItem
3.4. Return to caller
4. Set m_state to Free
5. Return to caller
Der Logik können Sie entnehmen, dass bei der Rückgabe einer Callback-Methode das Gate überprüft, ob es sich um den letzten Leser handelt. Ist dies nicht der Fall, werden keine zusätzliche Methoden aufgerufen, und der Thread des Threadpools, der die Callback-Methode ausführt, wird an den Threadpool zurückgegeben, damit er für weitere Aufgaben verwendet werden kann.
Gibt es keine weiteren Leser, dann prüft das Gate, ob Write-Methoden in der Warteschlange m_qWriteRequests anstehen. Wenn dem so ist, wird die Callback-Methode des nächsten Schreibers an den CLR-Threadpool weitergeleitet. Dadurch wird ein Thread des Threadpools aktiviert, der den Code ausführt, der zum Schreiben in die Ressource eingesetzt wird. Beachten Sie hierbei, dass jeweils nur eine Write-Methode aufgerufen wird. Dies garantiert den exklusiven Zugriff auf die durch das Gate geschützten Daten. Bitte beachten Sie auch, dass meine Implementierung die Aktivierung von Schreibern immer vorrangig behandelt, was dazu führen kann, dass Leseanfragen benachteiligt werden. Doch dies ist die übliche Vorgehensweise bei den so genannten Leser-Schreiber-Sperren. Sie können diese Einstellung jederzeit ändern und bei Bedarf Lesern den Vorzug geben.
Sind Schreibanforderungen in der Warteschlange aufgelaufen, prüft das Gate, ob auch Reader-Methoden in der Warteschlange m_qReadRequests eingereiht sind. Ist dies der Fall, werden alle Callback-Methoden der Leser an den CLR-Threadpool weitergeleitet, da sie theorethisch alle gleichzeitig ausgeführt werden können. Sie erinnern sich vielleicht noch an das eingangs erwähnte erhebliche Leistungsproblem, wenn normale Leser-Schreiber-Sperren alle wartenden Lesethreads freigeben: Alle Threads sind ausführbar, und das Betriebssystem muss viele Kontextwechsel durchführen, was sich nachteilig auf die Leistung auswirkt.
Im Fall von ReaderWriterGate werden alle Callback-Methoden der Leser in die Warteschlange des Threadpools eingereiht, der geschickt mit nur wenigen Threads nur ein paar Methoden gleichzeitig aktiviert. Idealerweise entspricht die Anzahl der eingesetzten Threads der Anzahl der CPUs im Computer, sodass kein Kontextwechsel stattfinden muss. Diese wenigen Threads führen die Callback-Methoden der Leser durch und kehren dann zum Threadpool zurück, um weitere Methoden auszuführen. Im Endeffekt werden nur wenige Threads benötigt, um ein großes Arbeitsaufkommen zu bewältigen. Hinzu kommt, dass die Aufgaben noch schneller ausgeführt werden können, da weniger Kontextwechsel erforderlich sind. Bessere Leistung mit weniger Ressourcen – Sie werden begeistert sein!
So wissen Sie, wann der Callback abgeschlossen ist
Kurz, nachdem ich die erste Version meiner Power Threading Library mit meinem ReaderWriterGate veröffentlicht hatte, wurde versucht, diese in einer ASP.NET-Anwendung einzusetzen. Das Szenario ähnelte dem, das ich beschrieben hatte: Kunden forderten Webseiten an, und um die Skalierbarkeit zu erhöhen, mussten die Webseiten mithilfe von Leser-Schreiber-Semantik auf freigegebene Daten zugreifen können. Glücklicherweise unterstützt ASP.NET 2.0 die Implementierung von asynchronen Webseiten. Wenn Sie nähere Informationen zu diesem Thema wünschen, empfehle ich Ihnen Jeff Prosises
Wicked Code-Artikel (möglicherweise in englischer Sprache), der sich mit asynchronen Seiten in ASP.NET 2.0 befasst. Erschienen ist dieser Artikel in der Ausgabe vom Oktober 2005 des
MSDN®Magazins.
Um asynchrone Webseiten optimal zu nutzen, müssen Sie eine asynchrone Anforderung herausgeben und ein IAsyncResult-Objekt an die ASP.NET-Infrastruktur zurückgeben, um festzustellen, ob der asynchrone Vorgang abgeschlossen wurde. Mit den Methoden QueueWrite und QueueRead der ReaderWriterGate-Klasse starten Sie den asynchronen Vorgang. In meiner Original-Implementierung bot ich Ihnen jedoch keine Möglichkeit an zu erkennen, wann die Callback-Methode ihre Aufgabe tatsächlich abgeschlossen hat. Dies bedeutet, dass ReaderWriterGate nicht problemlos in einer Anwendung mit ASP.NET Web Forms eingesetzt werden kann.
Nachdem ich diese Rückmeldung erhalten hatte, erkannte ich, wie nützlich eine Lösung hier sein könnte, und ergänzte die ReaderWriterGate-Klasse durch ein paar zusätzliche Methoden. Dies sind die neuen Methoden:
public sealed class ReaderWriterGate
{
public IAsyncResult BeginRead(ReaderWriterGateCallback callback,
Object state, AsyncCallback asyncCallback, Object asyncState);
public void EndRead(IAsyncResult ar);
public IAsyncResult BeginWrite(ReaderWriterGateCallback callback,
Object state, AsyncCallback asyncCallback, Object asyncState);
public void EndWrite(IAsyncResult ar);
... // Other methods not shown
}
Sie verwenden statt der Methode „QueueWrite/QueueRead“ die Methoden „BeginWrite/BeginRead“ und „EndWrite/EndRead“ für die Erstellung einer asynchronen ASP.NET-Webseite oder irgendeiner anderen Anwendung, bei der Sie wissen möchten, wann die Callback-Methode abgeschlossen ist. Diese BeginXxx-Methoden werden immer umgehend zurückgegeben, doch erhalten Sie jetzt bei deren Rückgabe immer ein IAsyncResult-Objekt, das die in der Warteschlange eingereihte Callback-Methode identifiziert. Das zurückgegebene IAsyncResult-Objekt fügt sich optimal in das asynchrone CLR-Programmierungsmodell ein, und Sie können es wie jedes andere IAsyncResult-Objekt verwenden. Im Fall einer ASP.NET-Website können Sie mein Objekt direkt an die ASP.NET-Infrastruktur zurückgeben, und es wird die Verarbeitung der Seite abschließen, sobald die Callback-Methode abgeschlossen wurde. Darüber hinaus berücksichtige ich hier, genauso wie bei dem asynchronen CLR-Programmierungsmodell, beim Aufrufen von EndWrite/EndRead jede Ausnahme, die bisher von der Callback-Methode unbearbeitet geblieben ist.
Schlussbemerkung
Auf die Idee mit dem ReaderWriterGate und meine Implementierung bin ich sehr stolz. Meines Erachtens bietet diese Umsetzung eine praxisnahe Lösung für die Erstellung skalierbarer Anwendungen und Dienste mithilfe eines Minimums an Ressourcen. Selbst, wenn das Threading schon seit vielen Jahren eingesetzt wird, bin ich immer noch der Ansicht, dass es neue, viel versprechende Lösungsansätze für die Architektur von Anwendungen und Diensten gibt, durch die eine optimale Nutzung von Computern mit mehreren CPUs, die immer mehr im Kommen sind, erst möglich wird.
Senden Sie Fragen und Kommentare in englischer Sprache an Jeffrey mmsync@microsoft.com.
Jeffrey Richter ist Mitbegründer von Wintellect (
www.Wintellect.com), einer Firma zur Prüfung von Softwarearchitekturen, Beratung und Schulung. Er ist zudem Autor verschiedener Bücher, darunter auch der englischsprachige Titel „CLR via C#“ (Microsoft Press, 2006). Darüber hinaus schreibt Richter auch für das MSDN Magazin und ist seit 1990 beratend für Microsoft tätig.