War diese Seite hilfreich?
Ihr Feedback ist uns wichtig. Teilen Sie uns Ihre Meinung mit.
Weiteres Feedback?
1500 verbleibende Zeichen
Exportieren (0) Drucken
Alle erweitern
Erweitern Minimieren

Scope<T> und mehr

Von Stephen Toub

Laden Sie den Code zu diesem Artikel herunter: NetMatters2006_09.exe (155KB)

F: Ich habe mit der neuen TransactionScope-Klasse in .NET Framework 2.0 herumgespielt, und mir gefällt das bereitgestellte Modell. Zu Beginn einer Transaktion kann ich in einer Methode ein TransactionScope mit einer Transaction erstellen. Dann kann ich in einer anderen Methode, die von der ersten Methode aufgerufen wird, einen SqlCommand erstellen, der automatisch in dieser Transaction eingetragen wird. Wie wird dem SqlCommand mitgeteilt, dass es diese zuvor erstellte Transaction gibt? Und vor allem, wie kann ich diese Funktionalität in meinen eigenen Klassen nachahmen?

A: Für diejenigen, die nicht mit TransactionScope vertraut sind, sei gesagt, dass dies ein Teil vom System.Transactions-Namespace ist, bei dem es sich um eine Neuheit in Microsoft® .NET Framework 2.0 handelt. System.Transactions bietet ein voll in .NET Framework integriertes Transaktionsframework, einschließlich (jedoch nicht begrenzt auf) ADO.NET. Bei Transaction und TransactionScope handelt es sich um zwei der wichtigsten Klassen in diesem Namespace. Wie in der Frage angedeutet, können Sie eine TransactionScope-Instanz erstellen, und ADO.NET-Vorgänge, die im Rahmen dieser TransactionScope-Instanz ausgeführt werden, werden automatisch eingetragen (Sie können auf die aktuelle Transaction auch über die statische Transaction.Current-Eigenschaft zugreifen):

using(TransactionScope scope = new TransactionScope())
{
    ... // all operations here part of a transaction
    scope.Complete();
}

Es gibt mehrere Konstruktoren für TransactionScope. Einige akzeptieren einen TransactionScopeOption-Enumerationswert, der dem TransactionScope mitteilt, ob eine neue Transaktion erstellt werden soll, ob eine möglicherweise vorhandene Umgebungstransaktion verwendet werden soll oder ob alle Umgebungstransaktionen unterdrückt werden sollen. Durch das Ausführen des folgenden Codes würde ein verschachteltes TransactionScope erstellt, das eine neue Transaktion anstelle der vorhandenen Umgebungstransaktion erfordert. Zuerst wird die lokale Kennung der ursprünglichen Transaction ausgegeben. Darauf folgt die Kennung der neuen Transaction und dann erneut die ursprüngliche Kennung:

using (new TransactionScope())
{
    Console.WriteLine(
        Transaction.Current.TransactionInformation.LocalIdentifier);
    using (new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        Console.WriteLine(
            Transaction.Current.TransactionInformation.LocalIdentifier);
    }
    Console.WriteLine(
        Transaction.Current.TransactionInformation.LocalIdentifier);
}

Eine willkürliche Anzahl von verschachtelten TransactionScopes ist möglich. Intern wird der Stapel aller Umgebungstransaktionen vom System.Transaction-Namespace verwaltet.

Grundsätzlich liefern threadstatische Member dieses Modell. Wenn ein Typ ein nicht statisches Feld (ein Instanzfeld) enthält, verfügt jede Instanz dieses Typs über einen eigenen separaten Speicher für das Feld, d. h. wenn ich in einer Instanz einen Wert für dieses Feld festlege, wirkt sich das nicht auf den Wert des Felds in einer anderen Instanz aus. Im Gegensatz dazu existiert ein statisches Feld nur an einer Speicherstelle (oder genauer gesagt, nur an einer Speicherstelle pro Anwendungsdomäne), unabhängig von der Anzahl der Instanzen. Wenn jedoch System.ThreadStaticAttribute auf ein statisches Feld angewendet wird, wird es zu einem threadstatischen Feld. Das bedeutet, dass der Speicher für ein Feld von jedem Thread selbst (nicht der Instanz) verwaltet wird. Das Festlegen des threadstatischen Werts in einem Thread wirkt sich nicht auf den Wert in einem anderen Thread aus. Für diejenigen, die mit Visual C++® vertraut sind, sei gesagt, dass diese Funktionalität dem Konzept von __declspec(thread) ähnelt, das dafür verwendet wird, eine threadlokale Variable im threadlokalen Speicher zu deklarieren.

Threadstatische Felder werden für verschiedene Szenarios verwendet. Sie werden häufig dafür verwendet, einen Singleton zu speichern, dessen Typ nicht threadsicher ist. Durch das Erstellen eines Singleton für jeden Thread und nicht für die ganze Anwendungsdomäne muss kein explizites Sperren vorgenommen werden, um die Threadsicherheit sicherzustellen, da nur ein Thread auf die Variable und damit auf die Instanz zugreifen kann (diese Sicherheit ist jedoch nichtig, wenn der Thread eine Referenz für dieses Singleton-Objekt an einen anderen Thread übergibt). Bei der Arbeit mit statischen Singletons sind in der Regel zwei Sperren erforderlich: eine für die Initialisierung (entweder eine implizite Sperre, wenn die Initialisierung im statischen Konstruktor des Typs vorgenommen wird, oder eine explizite Sperre bei verzögerter Initialisierung) und eine für das Zugreifen auf die tatsächliche Instanz. Keine dieser Sperren ist bei threadstatischen Singletons erforderlich.

Ein weiterer Verwendungszweck für threadstatische Felder und einer, mit dem ich wieder auf TransactionScope zu sprechen komme, ist das Übergeben von Out-of-Band-Daten zwischen Methodenaufrufen. Bei nicht objektorientierten Sprachen kann eine Funktion in der Regel nur auf Daten zugreifen, die explizit durch Parameter oder globale Variablen bereitgestellt wurden. Dagegen gibt es bei objektorientierten Systemen eine Vielzahl von anderen Möglichkeiten, wie etwa Instanzfelder, wenn es sich bei der Methode um eine Instanzmethode handelt, oder statische Felder, wenn es sich um eine statische Methode handelt.

Es wäre vorstellbar, dass die aktuelle Transaction von TransactionScope in statischen Feldern gespeichert wird und dass diese Transaction von SqlCommand aus dem statischen Feld übernommen wird. In einem Multithread-Szenario könnte dies jedoch enorme Schwierigkeiten verursachen, da sich zwei TransactionScopes in die Quere kommen könnten. Ein Thread könnte die vor kurzem veröffentlichte Transaction-Referenz eines anderen Threads überschreiben. Das könnte dazu führen, dass mehrere Threads dieselbe Transaction-Instanz verwenden, und das könnte zu einer Katastrophe führen.

Hier erweisen sich threadstatische Felder als sehr hilfreich. Die in einem threadstatischen Feld gespeicherten Daten sind nur für Code sichtbar, der im selben Thread ausgeführt wird, in dem die Daten gespeichert sind. Daher können mit einem solchen Feld zusätzliche Daten von einer Methode an eine andere, von der ersten aufgerufenen Methode übergeben werden. Und dabei kann es zu keiner Beeinträchtigung durch andere Threads kommen. Stellen Sie sich jetzt vor, dass TransactionScope eine ähnliche Technik verwendet. Bei der Instanziierung könnte damit die aktuelle Transaction in einem threadstatischen Feld gespeichert werden. Wenn später ein SqlCommand instanziiert wird, bevor dieses TransactionScope aus dem threadlokalen Speicher entfernt wurde, kann der SqlCommand das threadstatische Feld auf eine vorhandene Transaction untersuchen und sich in dieser Transaction, falls vorhanden, eintragen. Auf diese Weise können TransactionScope und SqlCommand zusammenarbeiten, ohne dass der Entwickler explizit eine Transaction an das SqlCommand-Objekt übergeben muss. Tatsächlich ist der von TransactionScope und SqlCommand verwendete Mechanismus komplizierter, aber im Kern ist die Prämisse zutreffend.

Mithilfe von threadstatischen Feldern können Sie ein ähnliches System für jeden Typ erstellen. Abbildung 1 zeigt eine von mir geschriebene Klasse, Scope<T>, mit der ein ähnliches Verhalten implementiert wird. Der grundlegende Gedanke ist, dass Sie in einer Methode ein Scope<T> mit einer Instanz vom Typ T instanziieren können, auf den eine andere Methode später in der Aufrufliste zugreifen können soll. Diese Methode kann mit Scope<T>.Current auf die Instanz zugreifen. Das Verschachteln wird ebenfalls von Scope<T> unterstützt, d. h. dass es intern einen Stapel mit Instanzen vom Typ T verwaltet und die oberste Instanz im Stapel mithilfe der Current-Eigenschaft offen legt. Wenn so ein Scope<T> mit einer Instanz von T von einer Methode erstellt wird, von der dann eine andere Methode aufgerufen wird, durch die ein weiterer Scope<T> mit einer anderen Instanz von T instanziiert wird, dann geht die erste Instanz nicht verloren; auf sie kann weiterhin mithilfe von Scope<T>.Current zugegriffen werden, sobald der verschachtelte Scope widerrufen wurde.

Umgebungseigenschaften, wie etwa Scope<T>.Current, sollten fast immer streng begrenzt werden. Anders ausgedrückt werden sie im selben Stapelrahmen veröffentlicht und widerrufen, in der Regel durch einen try/finally-Block. Das gilt für TransactionScope, mit Sperren, mit Identitätswechsel usw. Und aus diesem Grund wird IDisposable von Scope<T> implementiert: Scope<T> wird in einem using-Block verwendet, und zwar so, dass die Instanz durch den Konstruktor veröffentlicht und die Dispose-Methode widerrufen wird.

Ich möchte das an einem Beispiel verdeutlichen. In Abbildung 2 sehen Sie ein Beispiel zur Verwendung von Scope<T> zusammen mit einer Instanz von StreamWriter. Die Main-Methode beginnt mit dem Ausgeben von MainEnter an den StreamWriter, der von Scope<StreamWriter>.Current zurückgegeben wurde. Da momentan kein Scope<StreamWriter> aktiv ist, gibt Scope<StreamWriter>.Current den Wert NULL zurück (aus diesem Grund überprüfe ich mit einer Write-Hilfsmethode, ob der StreamWriter den Wert NULL hat, bevor eine Ausgabe an den StreamWriter erfolgt, um die andernfalls unvermeidliche NullReferenceException zu vermeiden). Ein neuer Scope<StreamWriter> wird dann mit einem StreamWriter für die Datei C:\test1.txt instanziiert, und die FirstMethod-Methode wird aufgerufen. First-Method gibt FirstMethodEnter an den aktuellen StreamWriter aus. Dieser Text wird an C:\test1.txt ausgegeben, da es sich bei dieser Datei um den momentan im Bereich befindlichen StreamWriter handelt. Anschließend wird von FirstMethod ein neuer Scope<StreamWriter> eingerichtet, diesmal für C:\test2.txt. Im Scope<T> wird dadurch der neue StreamWriter auf den internen Stapel verschoben, und zwar auf den vorhandenen StreamWriter für C:\test1.txt. An diesem Punkt gibt Scope<StreamWriter>.Current eine Referenz an den StreamWriter für C:\test2.txt zurück, sodass der Aufruf an SecondMethod dazu führt, dass SecondMethod an C:\test2.txt und nicht an C:\test1.txt ausgegeben wird. Anschließend wird der Scope<StreamWriter> für C:\test2.txt FirstMethod widerrufen (wegen des using-Schlüsselworts), dieser StreamWriter wird aus dem internen Stapel entfernt, und der StreamWriter für C:\test1.txt wird wieder als aktueller StreamWriter hergestellt, sodass beim Übergeben von FirstMethodExit an Scope<StreamWriter>.Current dieser Text an C:\test1.txt ausgegeben wird. Schließlich wird in Main der ursprüngliche Scope<StreamWriter> widerrufen, wodurch der interne Stapel von StreamWritern geleert wird, sodass Scope<StreamWriter>.Current wieder den Wert NULL zurückgibt. Uff.

Intern benötigt Scope<T> ein threadstatisches Feld, um den an den Konstruktor von Scope<T> übergebenen Stack<T> von Instanzen zu speichern. Bei jeder Konstruktion eines Scope<T> wird der Stack<T> des Threads aus der Instances-Eigenschaft abgerufen, und der neue T wird darauf verschoben. Die statische Current-Eigenschaft verwendet die Stack<T>.Peek-Methode dafür, die T-Instanz oben auf dem Stapel, falls vorhanden, zurückzugeben. Falls der Stack<T> leer ist, wird der Wert NULL zurückgegeben. Sobald der Scope<T> widerrufen wird, wird das oberste Element vom Stapel entfernt.

Wenn Sie sich noch einmal Abbildung 1 ansehen, fragen Sie sich vielleicht, warum ich in Stack<T> für das Feld_instances die verzögerte Initialisierung verwende. Es muss schließlich erstellt werden, damit der Scope<T>-Konstruktor erfolgreich ausgeführt wird, daher wäre die verzögerte Initialisierung übertrieben, oder? Eigentlich nicht. Ich hätte die verzögerte Initialisierung entfernen und einfach die Felddeklaration dahingehend ändern können, dass die Initialisierung enthalten ist:

private static Stack<T> Instances { return _instances; }
[ThreadStatic]
private static Stack<T> _instances = new Stack<T>();

Wenn ich jedoch diese Änderung vorgenommen hätte und Scope<T> von mehreren Threads verwendete, dann würden beim Versuch, neue Scope<T>-Instanzen zu konstruieren, möglicherweise NullReferenceExceptions zurückgegeben. Obwohl das Feld mit dem Attribut ThreadStatic gekennzeichnet ist, gehört die Initialisierung des Felds zum statischen Konstruktor des Typs. Daher wird sie nur einmal ausgeführt, und zwar auf dem Thread, auf dem der statische Konstruktor des Typs ausgeführt wird (der erste Thread, der das Rennen um den Zugriff auf seinen Status gewinnt). Scope<T> wird problemlos auf diesem Thread funktionieren, da das Feld _instances korrekt initialisiert wurde. Auf allen anderen Threads wird _instances jedoch weiterhin den Wert NULL zurückgeben. Dadurch wird jedes Mal, wenn ein Scope<T>-Konstruktor versucht, ein Element auf den nicht vorhandenen Stapel zu verschieben, NullReferenceExceptions zurückgegeben.

Es gibt einige weitere Implementierungsdetails, die Sie ebenfalls beachten sollten. Nachdem eine Instanz vom Stapel entfernt wurde, wird von der Dispose-Methode geprüft, ob es noch weitere Instanzen im Stapel gibt. Ist dies nicht der Fall, wird das threadstatische Stack<T>-Feld auf NULL gesetzt. Threadstatische Felder haben die schöne Eigenschaft, dass sie nicht mehr als GC-Stamm behandelt werden, nachdem der verwaltete Thread nicht mehr vorhanden ist. Wenn jedoch Scope<T> selten von einem Thread verwendet wird und eine lange Lebensdauer hat, bleibt der enthaltene Stack<T> möglicherweise viel länger als notwendig erhalten, und wenn Scope<T> im Laufe der Zeit von vielen Threads verwendet wird, könnte dies zu einer Aufblähung führen. Daher bevorzuge ich, den Stapel nur so lange zu erhalten, wie er Instanzen enthält, und ihn freizugeben, sobald er leer ist.

Außerdem sind die Aufrufe an die statischen Methoden BeginThreadAffinity und EndThreadAffinity auf der Thread-Klasse zu beachten. Diese Methoden sind vorhanden, weil es CLR-Hosts in .NET Framework 2.0 ursprünglich möglich sein sollte, selbst Threads zu verwalten. Ein Host könnte verwaltete Threads als Fibers ausführen und selbst die Fiberplanung vornehmen. Daher könnte ein CLR-Host jederzeit eine auszuführende Aufgabe von einem physischen BS-Thread zu einem anderen verschieben. Aber abhängig davon, was wirklich vom verwalteten Code ausgeführt wurde, könnte für die ausgeführten Aufgaben Threadaffinität erforderlich sein, d. h. für die Dauer der Aufgabe müssen sie auf demselben physischen BS-Thread bleiben. Aus diesem Grund wurden die Methoden BeginThreadAffinity und EndThreadAffinity eingeführt, mit denen ein verwalteter Code einem Host mitteilen konnte, dass eine Aufgabe nicht verschoben werden sollte, solange die Threadaffinität erforderlich war. Da Scope<T> einen threadlokalen Speicher verwendet (der einem physischen Thread und nicht einer Fiber zugeordnet ist), ist es wichtig, diese Methoden zu nutzen. Andernfalls könnte es bei der Ausführung auf einem Thread vorkommen, dass Scope<T> Daten in einem threadstatischen Feld speichert. Anschließend könnte die Aufgabe, ohne dass es Scope<T> bekannt ist, auf einen anderen BS-Thread mit einem völlig anderen threadstatischen Wert verschoben werden. Da sich die Unterstützung von Fibern nicht durchgesetzt hat, schadet es momentan nicht, diese Methoden nicht korrekt aufzurufen. Es ist jedoch relativ wahrscheinlich, dass die Unterstützung für Fibern in einer zukünftigen Version von Framework wieder eingeführt wird. Wenn Sie sich also jetzt daran gewöhnen, diese Methoden zu verwenden, können Sie sich zukünftig einige ernsthafte Debuggingprobleme ersparen.

Beachten Sie schließlich auch, dass Scope<T> die bereitgestellte Instanz zusätzlich zum Verschieben auf den Stack<T> in einem Instanzfeld speichert. Damit wird lediglich überprüft, ob Scope<T>s korrekt begrenzt und in der richtigen Reihenfolge entfernt wurden. Nachdem eine Instanz aus einem Stapel entfernt wurde, wird sie mit der in Scope<T> zwischengespeicherten Instanz verglichen; sie sollten immer gleich sein. Ist dies nicht der Fall, bedeutet dies, dass verschachtelte Scope<T>-Instanzen in der falschen Reihenfolge widerrufen wurden.

Ist Stack<T> aber überhaupt notwendig, nachdem jetzt die vom Benutzer bereitgestellte Instanz in Scope<T> gespeichert wird,? Tatsächlich ist es das nicht. Verschachtelte Scope<T>-Instanzen können in einer Liste verkettet werden, wobei der Listenanfang im threadstatischen Feld gespeichert wird und jeder Scope<T> eine Referenz zum vorausgehenden Scope<T> speichert. Das vereinfacht nicht nur den Entwurf, sondern einige der für die Verwaltung von Stack<T> jetzt nicht mehr benötigten Zuordnungen werden entfernt. In Abbildung 3 wird die aktualisierte Version von Scope<T> dargestellt.

F: Mir gefällt die neue Semaphore-Klasse in .NET Framework, sie ist aber nicht so einfach zu verwenden wie die Monitor-Klasse, in der C# und Visual Basic® die lock- und SyncLock-Schlüsselwörter bereitstellen. Warum bietet Semaphore keine ähnliche Funktionalität?

A: Mithilfe eines kleinen Codes wird es genauso einfach. In Abbildung 4 sehen Sie eine leichte Wrapperklasse, mit der Sie das lock/SyncLock-Verhalten nachahmen können, nur eben mit einem Semaphore und nicht einem Monitor. Bei Disposable.Lock handelt es sich um eine statische Methode, die ein Semaphore akzeptiert, darauf wartet und dann die neue Instanz einer Klasse zurückgibt, die IDisposable implementiert. Durch das Aufrufen von Dispose bei dieser Instanz wird der Semaphore freigegeben. Dadurch können Sie das using-Schlüsselwort mit einem Semaphore verwenden, um darauf zu warten, eine Aufgabe auszuführen und ihn dann freizugeben:

using(Disposable.Lock(theSemaphore))
{
    ... // do work here
}

Die Implementierung in Abbildung 4 ist einfach, aber es werden zwei Methoden verwendet, mit denen Sie möglicherweise nicht vertraut sind: Thread.BeginCriticalRegion und Thread.EndCriticalRegion. In einem kritischen Bereich sind die Auswirkungen einer asynchronen oder unbehandelte Ausnahme möglicherweise nicht auf die aktuelle Aufgabe begrenzt, sondern könnten die gesamte Anwendungsdomäne destabilisieren. Beispiel: Ein Thread wird bei der Bearbeitung von threadübergreifenden Daten unterbrochen, sodass nicht alle Daten in einen gültigen Status zurückgesetzt werden können. Wenn ein Fehler (wie etwa ThreadAbortException) in einem kritischen Bereich auftritt, kann ein CLR-Host, wie etwa SQL Server 2005, die gesamte Anwendungsdomäne abbrechen statt das Risiko einzugehen, die Ausführung mit einem potenziell unstabilen Status fortzusetzen. Sie müssen dem CLR mitteilen, wann der Thread in einem kritischen Bereich ist und wann er ihn verlässt, sodass, falls eine Fehlerbedingung auftreten sollte, der Host entsprechend vom CLR informiert werden kann. Aus diesem Grund gibt es Thread.BeginCriticalRegion und Thread.EndCriticalRegion. Da Sperren für die Kommunikation zwischen Threads verwendet werden, ist es nahe liegend, dass der kritische Bereich mit dem Herausnehmen einer Sperre beginnt und mit der Freigabe der Sperre endet. Diese Logik ist in Monitor integriert, daher habe ich sie meine Implementierung von Disposable.Lock in Abbildung 4 einbezogen. Weitere Informationen finden Sie in meinem Artikel in der MSDN ® Magazine-Ausgabe vom Oktober 2005 unter msdn.microsoft.com/msdnmag/issues/05/10/Reliability (in englischer Sprache).

Senden Sie Fragen und Kommentare für Stephen in englischer Sprache an  netqa@microsoft.com.

Stephen Toub ist der technische Redakteur für MSDN-Magazin.

Aus der Ausgabe vom September 2006 des MSDN-Magazins.

Anzeigen:
© 2015 Microsoft