5.3 Gesicherter Datenaustausch 

Wenn mehrere Aktivitäten in einem Programm gleichzeitig auf dieselben Daten zugreifen können, entstehen sehr leicht ungewollte Zustände – es verderben sozusagen viele Köche den Brei. Die Probleme, die mit dem unkoordinierten gleichzeitigen Zugriff zusammenhängen, sind die Motivation für die in diesem Abschnitt gezeigten Mechanismen zur Absicherung der Daten. Sehen wir uns die Problematik an dem folgenden Beispiel an. Es zeigt, wie abenteuerlich einfache Schleifen im Kontext mehrerer Threads werden können. Die Methode Test() startet in zwei verschiedenen Threads die Methode Druck(), die in einer Schleife eine Zählervariable no nutzt, auf die beide Threads direkt zugreifen können.
'...\ Multitasking\Datenaustausch\Unsynchronisiert.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Unsynchronisiert
Private no As Integer
Sub Druck()
For Unsynchronisiert.no = 1 To 40
Console.Write(no & " ")
Next
End Sub
Sub Test()
Dim erster As New Thread(AddressOf Druck)
Dim zweiter As New Thread(AddressOf Druck)
erster.Start()
zweiter.Start()
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe zeigt, wie die beiden Threads nacheinender in die Konsole Zahlen ausgeben, wobei sie bei null beginnen. Die Ausgaben des ersten Threads sind kursiv gesetzt. Welcher Thread jeweils zu welcher Zahl kommt, kann von Lauf zu Lauf variieren. Auf einem schnellen Rechner müssen Sie eventuell das Schleifenende höher als 40 setzen, um die beschriebenen Effekte zu sehen.
1 2 3 4 5 6 7 8 9 10 11 12 13 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 37 38 39 40 36
Der erste Thread kann alle Zahlen bis 13 ausgeben, aber nicht mehr den gemeinsamen Zähler no erhöhen. Dann wird er vom Scheduler des Betriebssystems unterbrochen, und der zweite Thread beginnt mit der Ausgabe. Er hat Zeit genug, alle Zahlen bis 35 auszudrucken und den gemeinsamen Zähler no auf 36 zu erhöhen und den Aufruf Write() vorzubereiten, aber nicht die Zahl auszugeben. Nun kommt wieder der erste Thread zum Zuge, holt die Erhöhung des Zählers nach und fährt fort. Da der zweite Thread noch dazu gekommen ist, den gemeinsamen Zähler no auf 36 zu erhöhen, ist die nächste vom ersten Thread gedruckte Zahl 37. Nachdem der erste Thread die 40 ausgegeben hat, ist er fertig, und der zweite Thread kann die Ausgabe der Zahl 36 nachholen. Im folgenden Schleifenkopf hat der gemeinsame Zähler no durch den ersten Thread bereits die 40 erreicht, und die Schleife wird beendet.
Hinweis |
Es gibt nur ganz wenige Operationen, in denen kein Wechsel des Threads passieren kann. Sie werden atomar genannt. So dürfen sogar zum Beispiel += und das Lesen und Schreiben eines Wertes vom Typ Long oder Double nicht atomar sein. |
Um solch ein Chaos zu verhindern, sollte der Programmcode so gestaltet werden, dass er auch dann funktioniert, wenn mehrere Threads im Spiel sind. Solch ein Code wird threadsicher genannt. Sind keine konkurrierenden Datenzugriffe möglich, zum Beispiel weil verschiedene Threads keine Daten gemeinsam nutzen, ist der Code inhärent threadsicher. In den anderen Fällen bietet das .NET Framework zwei Vorgehensweisen an:
- Eine Klasse übernimmt die Überwachung von Objekten.
- Aus gleichnamigen Variablen werden durch Zusatzangaben verschiedene Objekte.
Der erste Weg ist wesentlich flexibler. Da hier Aufgaben koordiniert werden, spricht man auch von Synchronisation. Unabhängig von der konkreten Überwachungsklasse wird einer Überwachungsmethode ein zu überwachendes Objekt übergeben. Diese kümmert sich dann darum, dass nicht zu viele Threads gleichzeitig auf das Objekt zugreifen. Damit das auch funktioniert, müssen Sie in Ihren Programmen sicherstellen, dass alle Zugriffe von einer Überwachungsklasse koordiniert werden.
Hinweis |
Eine Überwachungsklasse muss explizit beauftragt werden. Jede Aktion, die die Überwachung nicht durchläuft, unterwandert die Synchronisation. Threadsicherheit erfordert also eine aktive Unterstützung durch den Programmierer. |
Die Überwachung eines Objekts kann auch einen Programmteil vor zu vielen konkurrierenden Zugriffen schützen. Dazu rufen Sie direkt davor eine Überwachungsmethode auf, die ein als Parameter angegebenes Objekt reserviert. Kommt nun ein zweiter Thread an die gleiche Stelle im Programm, versucht es auch, das Objekt zu reservieren. Die meisten Überwachungsmethoden erlauben nur eine einfache Reservierung und versetzen den zweiten Thread in den Zustand wartend. Damit dieser Zustand nicht ewig andauert, müssen Sie am Ende des synchronisierten Codeabschnitts die Reservierung durch eine andere Überwachungsmethode wieder aufheben, die den zweiten Thread in den Zustand bereit versetzt. Die Struktur eines synchronisierten Quelltexts lautet also formal:
Reservierung(Objekt) |
Bei der Reservierung gibt es zwei spezielle Objekttypen:
- Me: Damit sperren Sie das gesamte Objekt, in dem sich die Methode mit dem synchronisierten Codeabschnitt befindet; wenn nur Teile einer Klasse gesperrt werden müssen, sollte es nicht verwendet werden, um andere Threads nicht unnötig zu behindern.
- objekt.GetType(): Da GetType() für Objekte desselben Typs immer dasselbe Objekt liefert, können Sie damit die mit Shared an die Klasse gebundenen Variablen sperren.
Hinweis |
Durch das automatische Boxing von Werttypen werden beim Aufruf der Überwachungsmethoden Kopien erstellt. Diese Kopien können niemals zum Sperren verwendet werden, denn es wird einerseits nur eine Kopie gesperrt, und andererseits kann die Kopie nie freigegeben werden, da keine Referenz auf sie außerhalb der Sperrmethode existiert. |
In den folgenden Unterabschnitten zeige ich einige konkrete Möglichkeiten, Objekte zu reservieren und Daten nach Threads getrennt zu speichern.
5.3.1 Objekte sperren mit Monitor 

Die Klasse Monitor ist eine sehr einfache Möglichkeit, Threads zu synchronisieren. Das durch Überwachungsmethoden reservierte Objekt ist immer im Besitz von maximal einem Thread. Die auferlegte Sperre ist also exklusiv. Alle mit der Klasse Monitor zu synchronisierenden Threads müssen sich im selben Betriebssystemprozess befinden.
Enter und Exit
Das unsynchronisierte Beispiel aus der Einführung lässt sich durch zwei Zeilen synchronisieren. Vor Eintritt in die Schleife wird die Zählervariable durch Monitor.Enter() gesperrt. Jeder nachfolgende Aufruf, hier durch den zweiten Thread, blockiert so lange, bis das Objekt wieder freigegeben ist. Als Werttyp ist die Zählervariable selbst ungeeignet zum Sperren. Es kann aber der gesamte Datentyp gesperrt werden. Da die Zählervariable in Module implizit an den Datentyp gebunden ist, wird sie damit auch gesperrt. Im folgenden Codefragment besorgen wir uns mit GetType() eine Referenz auf den Datentyp Synchronisiert. Nachdem die Schleife durchlaufen wurde, wird das Objekt, hier ein Datentyp, mit Monitor.Exit() wieder freigegeben. Vergessen Sie die Freigabe, bleibt der zweite Thread an der ersten Zeile in Druck() hängen. Der Methodenrumpf wird in einen Try/Finally-Block eingeschlossen, damit beim Auftreten einer Ausnahme das Objekt in jedem Fall durch Exit() zurückgegeben wird und nicht andere Threads blockiert. In Ihrem Quelltext sollten Sie die Objektrückgabe immer in einen Finally-Zweig schreiben. Ich verzichte hier meistens darauf, um die Beispiele kurz zu halten.
'...\ Multitasking\Datenaustausch\Synchronisiert.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Unsynchronisiert
Private no As Integer
Sub Druck
Monitor.Enter(GetType(Synchronisiert))
Try
For Synchronisiert.no = 1 To 40
Console.Write(no & " ")
Next
Finally
Monitor.Exit(GetType(Synchronisiert))
End Try
End Sub
Sub Test()
Dim erster As New Thread(AddressOf Druck)
Dim zweiter As New Thread(AddressOf Druck)
erster.Start()
zweiter.Start()
Console.ReadLine()
End Sub
End Module
End Namespace
Die Zahlen werden nun zusammenhängend ausgegeben, da durch die Sperre mit Monitor nicht ein Thread dem anderen dazwischenfunken kann.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
29 30 31 32 33 34 35 36 37 38 39 40 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
Als abschreckendes Beispiel verändert das nächste Codefragment das Beispiel leicht. Es hat zwei Methoden, die denselben Schleifenzähler no verwenden, wobei eine Methode mittels der Klasse Monitor synchronisiert ist und die andere nicht.
'...\ Multitasking\Datenaustausch\Teilsynchronisiert.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Teilsynchronisiert
Private no As Integer
Sub Druck1()
Monitor.Enter(GetType(Synchronisiert))
For Teilsynchronisiert.no = 1 To 40 : Console.Write(no & " ") : Next
Monitor.Exit(GetType(Synchronisiert))
End Sub
Sub Druck2()
For Teilsynchronisiert.no = 1 To 40 : Console.Write(no & " ") : Next
End Sub
Sub Test()
Dim erster As New Thread(AddressOf Druck1)
Dim zweiter As New Thread(AddressOf Druck2)
erster.Start()
zweiter.Start()
Console.ReadLine()
End Sub
End Module
End Namespace
Das Resultat ist das gleiche Durcheinander wie im vollkommen unsynchronisierten Fall:
1 2 3 4 5 6 7 8 9 10 11 12 13 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 37 38 39 40 36
Hinweis |
Je nachdem, wie lange ein Thread zum Starten braucht und wie das Betriebssystem die Rechenzeiten verteilt, kann die Reihenfolge in der Ausführung von der der Start()-Aufrufe abweichen. Brauchen Sie eine bestimmte Reihenfolge, müssen Sie diese Art der Synchronisation zusätzlich programmieren (siehe nächstes Beispiel). |
TryEnter
Es gibt Situationen, in denen Sie eine beliebig lange Blockade beim Versuch einer doppelten Objektreservierung mit Monitor.Enter() vermeiden wollen. Insbesondere haben Benutzer wenig Verständnis dafür, dass aufgrund eines Programmierfehlers ein Programm vollständig blockiert, anstatt nur an einer Teilaufgabe zu scheitern. Um robustere Programme zu erhalten, sollten Sie statt Enter() besser TryEnter() verwenden. Ohne optionale Zeitangabe kehrt die Methode sofort zurück und gibt True zurück, wenn das als Parameter gegebene Objekt reserviert werden konnte, und False, wenn nicht. Eine optionale Zeitangabe spezifiziert, wie lange die Reservierung versucht werden soll. Um die Beispiele kurz zu halten, verzichte ich meistens auf die Verwendung von TryEnter().
Das folgende Codefragment simuliert die Benutzung eines Bades mittels der Methode Waschen(). Wenn innerhalb der gegebenen Zeitspanne namens zeit das Bad reserviert werden kann, kann 100 Millisekunden lang geduscht werden. In der Methode Test() werden vier Threads gestartet mit Wartezeiten von 0, 0, 80 und 200 Millisekunden.
'...\ Multitasking\Datenaustausch\Versuch.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Versuch
Private bad As New Object()
Sub Waschen(ByVal warten As Object)
Dim zeit As Integer = CType(warten, Integer)
If Monitor.TryEnter(bad, zeit) Then
Try
Thread.Sleep(100)
Console.WriteLine("{0} Millisekunden geduscht.", zeit)
Finally
Monitor.Exit(bad)
End Try
Else
Console.WriteLine("Noch besetzt nach {0} Millisekunden.", zeit)
End If
End Sub
Sub Test()
For Each zeit As Integer In New Integer() {0, 0, 80, 200}
Dim person As New Thread(AddressOf Waschen)
person.Start(zeit)
Next
Console.ReadLine()
End Sub
End Module
End Namespace
Nach ausreichend langer Wartezeit kommt jeder einmal dran. Ob zum Beispiel die 80 Millisekunden reichen, kann von Lauf zu Lauf variieren.
Noch besetzt nach 0 Millisekunden.
Noch besetzt nach 80 Millisekunden.
Nach 0 Millisekunden geduscht.
Nach 200 Millisekunden geduscht.
Wait und Pulse
Wenn Monitor.Enter() ein Objekt gesperrt hat und zur weiteren Verarbeitung ein anderer Thread etwas mit dem Objekt machen muss, kann die Sperre mit der Methode Wait() temporär aufgegeben und auf die Fertigstellung im zweiten Thread gewartet werden. Die Methode stellt den Thread in eine von Wait() verwaltete Warteschlange und hält den Thread an. Ein anderer Thread kann daraufhin das Objekt sperren und darauf zugreifen. Nach getaner Arbeit gibt er die Sperre auf und befördert den wartenden Thread von der Wait()-Warteschlage in die Warteschlange des Prozessors, sodass er weitermachen kann. Es kann mit Pulse() ein beliebiger Thread oder mit PulseAll() alle auf das Objekt wartenden Threads aus der Wait()-Warteschlange bewegt werden:
Sub Pulse(obj As Object) |
Rufen Sie Pulse() oder PulseAll() auf, ohne dass das Objekt zuvor mit Monitor.Enter() gesperrt wurde, wird die Ausnahme SynchronizationLockException ausgelöst. Anders als bei der Methode Wait() wird der Thread nach dem Aufruf von Pulse() oder PulseAll() nicht angehalten. Hängen zwei Threads voneinander ab, ist es daher sinnvoll, nach dem Aufruf auch im zweiten Thread die Methode Wait() aufzurufen, um wiederum auf den ersten Thread zu warten. Die wichtigsten Überladungen der Methode sind folgende (optionale Argumente stehen in eckigen Klammern):
Function Wait(obj As Object[, timeout As Integer]) As Boolean |
Im ersten Argument wird das Objekt angegeben, für das bereits durch Monitor.Enter() eine Sperre existiert. Rufen Sie Wait() auf, ohne dass eine solche Sperre existiert, wird die Ausnahme SynchronizationLockException ausgelöst. Ein Thread kann die Warteschlange von Wait() nur verlassen, wenn
- ein anderer Thread ihn über Pulse() oder PulseAll() bewegt
- die angegebene Zeitspanne abgelaufen ist (gegebenenfalls wurde Pulse() / PulseAll() vergessen)
Das Verlassen der Warteschlange von Wait() führt noch nicht zum Abschluss des Aufrufs von Wait() und damit zur Fortsetzung des ersten Threads, sondern macht die Fortsetzung nur möglich. Die Methode Wait() blockiert in jedem Fall weiterhin, bis das Objekt frei ist. Erst dann setzt der erste Thread seine Arbeit fort.
Hinweis |
Ein beliebiger Thread kann ein freies Objekt (wieder) sperren. Er muss überhaupt nichts mit der Benachrichtigung durch Pulse() und PulseAll() zu tun haben. Das geschieht frei nach dem Motto: Wer zuerst kommt, mahlt zuerst. |
Schauen wir uns dazu ein Beispiel an. Es zeigt einen Thread, der eine Zahl generiert, und einen zweiten, der sie verbraucht. Um das Beispiel kurz zu halten, befindet sich alles in einem einzigen Module. Durch die implizite Bindung in einem Module an den Datentyp haben beide Methoden Erzeuger() und Verbraucher() Zugriff auf die Variable Zahl. Damit keine Zahlen verloren gehen, soll vor der Produktion einer Zahl die vorhergehende verbraucht sein. Die Synchronisation kann nicht über die direkte Sperrung der Zahl erfolgen, da sie ein Werttyp ist, der bei Übergabe an Wait() und Pulse() kopiert würde. Eine Lösung ist, die Zahl in eine Klasse zu verpacken, damit die Sperre einen Referenztyp betrifft und nicht die Kopie eines Werttyps. Ich wähle als Alternative den Einsatz eines Dummyobjektes namens Sperre, das keine andere Aufgabe hat, als als Sperre zu dienen.
In der Methode Test() werden die beiden Threads in beliebiger Reihenfolge gestartet. Sowohl Erzeuger() als auch Verbraucher() sperren das Objekt Sperre am Methodenanfang mit Monitor.Enter() und geben es am Methodenende mit Monitor.Exit()wieder frei. Diese Sperre ist die Voraussetzung für den Gebrauch von Wait() und Pulse().
In der Schleife generiert Erzeuger() eine Zufallszahl und gibt sie aus. Der Aufruf Pulse() bewegt den Verbraucher aus der Warteschlange von Wait(). Nur wenn der Thread Verbraucher noch nicht den Fuß seiner Do-Schleife erreicht hat, kann er sich nicht in der Warteschlange befinden und der Aufruf von Pulse() verpufft wirkungslos. Dies tritt nur auf, wenn der Thread Erzeuger zuerst startet sowie mit Monitor.Enter() das Objekt Sperre reserviert und der Aufruf Monitor.Enter() den Thread Verbraucher blockiert, da bereits der Thread Erzeuger das Objekt sperrt. Startet zuerst der Thread Verbraucher und reserviert das Objekt, wird der Thread Erzeuger bereits vor Eintritt in die Schleife blockiert. Schließlich reiht sich der Thread Erzeuger durch den Aufruf von Wait() in die Warteschlange ein und erlaubt es dem Thread Verbraucher, überhaupt zu starten beziehungsweise die Auswertung des Aufrufs von Wait() im Schleifenfuß abzuschließen.
Der Rumpf der Schleife in Verbraucher wird durch die Fußsteuerung in jedem Fall erst einmal ausgeführt. Die Zahl wird ausgegeben und der Thread Erzeuger aus der Warteschlange von Wait() bewegt. Im Schleifenfuß begibt sich der Thread Verbraucher mit dem Aufruf Wait() selbst in die Warteschlange, damit der Thread Erzeuger wieder das Objekt Sperre exklusiv nutzen darf und die nächste Zahl erzeugen kann. Wenn der Thread Verbraucher das Objekt Sperre reserviert, bevor der Thread Erzeuger die erste Zahl generiert hat, ist diese noch null, und die If-Bedingung unterbindet die Ausgabe. Das Zeitlimit von 2000 Millisekunden im Schleifenfuß sorgt dafür, dass nach der Freigabe des Objekts Sperre durch den Aufruf von Monitor.Exit() am Ende der Methode Erzeuger() der Aufruf von Wait() nach zwei Sekunden mit dem Ergebnis False zurückkommt und die Schleife in Verbraucher() beendet.
'...\ Multitasking\Datenaustausch\Zahlenkonsument.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Zahlenkonsument
Private Zahl As Integer = 0
Private Sperre As New Object()
Sub Erzeuger()
Dim rnd As New Random()
Monitor.Enter(Sperre)
For no As Integer = 1 To 5
Zahl = rnd.Next(100, 999)
Console.Write("Zahl {0} erzeugt", Zahl)
Monitor.Pulse(Sperre)
Monitor.Wait(Sperre)
Next
Monitor.Exit(Sperre)
End Sub
Sub Verbraucher()
Monitor.Enter(Sperre)
Do
If Zahl > 0 Then Console.WriteLine(" und Zahl {0} verbraucht", Zahl)
Monitor.Pulse(Sperre)
Loop While Monitor.Wait(Sperre, 2000)
Monitor.Exit(Sperre)
End Sub
Sub Test()
Dim e As New Thread(AddressOf Erzeuger)
Dim v As New Thread(AddressOf Verbraucher)
e.Start()
v.Start()
Console.ReadLine()
End Sub
End Module
End Namespace
Jede generierte Zahl wird auch direkt verbraucht.
Zahl 579 erzeugt und Zahl 579 verbraucht
Zahl 965 erzeugt und Zahl 965 verbraucht
Zahl 175 erzeugt und Zahl 175 verbraucht
Zahl 998 erzeugt und Zahl 998 verbraucht
Zahl 545 erzeugt und Zahl 545 verbraucht
Sollte der Thread Erzeuger über zwei Sekunden für die Erzeugung einer Zahl brauchen, wird die Schleife in Verbraucher() beendet, und der Thread Erzeuger friert ein, da ihn niemand aus der Warteschlange holt, in die er sich durch den Aufruf von Wait() begeben hat. Ich habe die Variante gewählt, um das Beispiel kurz zu halten und auf das Problem aufmerksam zu machen. Robuster ist es, Wait() ohne Zeitlimit aufzurufen und nach dem Ende der Schleife in Erzeuger() die Zeilen
Zahl = –1
Monitor.Pulse(Sperre)
einzufügen sowie nach dem Do in Verbraucher():
If Zahl < 0 Then Exit Do
Die Klasse Monitor
Tabelle 5.4 fasst die Methoden der Klasse Monitor noch einmal zusammen. Sie sind alle mit Shared klassengebunden. Die Variable <timeout> repräsentiert die Alternativen millisecondsTimeout As Integer, timeout As TimeSpan und Timeout.Infinite. Wait, Pulse und PulseAll dürfen nur in einem mit Enter, Exit oder SyncLock synchronisierten Abschnitt stehen.
Methode | Beschreibung |
Enter(obj As Object) |
Exklusive Reservierung eines Objekts. Blockiert den Thread, wenn das Objekt bereits reserviert ist. |
Exit(obj As Object) |
Freigabe der Reservierung (wenn Exit genauso oft für das Objekt aufgerufen wird wie zuvor Enter) |
TryEnter(obj As Object, <timeout>) As Boolean |
Wie Enter(), aber Ende der Blockade nach timeout mit Rückgabe von False (True bei erfolgreicher Reservierung) |
Wait(obj As Object, <timeout>, exitContext As Boolean) As Boolean |
Sperrung eines Objekts aufheben und in die Wait-Warteschlange einreihen. Wenn das Objekt erst nach timeout frei ist, False zurückgeben und den Thread an die erste Stelle der Prozessorwarteschlange setzen (erfasst vergessenes Pulse()). Wenn der Stack ein Objekt mit dem Attribut Context enthält, wird mit exitContext temporär zum Standardstack gewechselt. |
Pulse(obj As Object) |
Den ersten Thread der Wait-Warteschlange an das Ende der Prozessorwarteschlange stellen |
PulseAll(obj As Object) |
Alle wartenden Threads in die Prozessorwarteschlange stellen |
5.3.2 Codebereiche mit SyncLock sperren 

Das Schlüsselwort SyncLock nimmt Ihnen die Arbeit ab, die Methoden Monitor.Enter() und Monitor.Exit() explizit aufzurufen sowie den Aufruf von Exit() in einen Finally-Zweig zu packen. Der Quelltext
Monitor.Enter(objekt)
Try
'Anweisungen
Finally
Monitor.Exit(objekt)
End Try
kann mit der Syntax von SyncLock kürzer geschrieben werden:
SyncLock objekt |
Das folgende Codefragment zeigt SyncLock im Einsatz. Es sperrt mittels Me das ganze Objekt Mikrofon für einen Redner.
'...\ Multitasking\Datenaustausch\Mikrofon.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Class Mikrofon
Sub Arbeit(ByVal rede As Object)
SyncLock Me
Thread.Sleep(100)
Console.WriteLine(rede)
End SyncLock
End Sub
Shared Sub Test()
Dim kond As New Mikrofon()
Dim redner As New Thread(AddressOf kond.Arbeit)
Dim zwischenruf As New Thread(AddressOf kond.Arbeit)
redner.Start("Um zum Ende meiner Rede zu kommen ...")
zwischenruf.Start("Kommen Sie zum Punkt.")
Console.ReadLine()
End Sub
End Module
End Namespace
Der Zwischenrufer muss sich hinten anstellen:
Um zum Ende meiner Rede zu kommen ...
Kommen Sie zum Punkt.
5.3.3 Sperrung mit Zustand 

Die Klasse Monitor erlaubt die zustandslose Kontrolle von Threads. Es gibt noch weitere Klassen zur Steuerung von Threads, die zusätzlich noch einen Zustand mitspeichern und so eine feinere Steuerung erlauben. Außerdem kann den meisten dieser Klassen zur Sperrung noch ein Name mitgegeben werden. Damit wird die Sperre betriebssystemweit gültig und kann auch über Prozessgrenzen hinweg verwendet werden. Der folgende Ausschnitt aus der Vererbungshierarchie zeigt die Klassen zur zustandsbehafteten Sperrung. Die beiden ersten sind im Namensraum System, die anderen in System.Threading.
Object
+-MarshalByRefObject
+-+RegisteredWaitHandle
+Timer
+WaitHandle
+-+EventWaitHandle
| +-+AutoResetEvent
| +ManualResetEvent
+Mutex
+Semaphore
In diesem Abschnitt gehe ich auf WaitHandle und deren Kindklassen ein. Sie haben eine gemeinsame Wirkungsweise, die auf dem Austausch von Signalen basiert. Ein Thread ruft eine Methode zum Sperren auf und wartet auf ein Signal zur Freigabe. Sowie eine Sperre im Zustand signalisiert ist, kann der Programmcode des Threads fortfahren. Manche Autoren vergleichen dies mit einer Verkehrsampel. Ich bevorzuge den Vergleich mit einem Drehkreuz am Eingang eines öffentlichen Gebäudes, das den Zugang beschränkt. Dies erfasst sowohl den Fall eines einzelnen Besuchers als auch einer Gruppe. Außerdem wird der Fall einer Ressourcenbeschränkung abgebildet. In der Realität kann dies ein Museum sein, das den Zugang zu wertvollen Exponaten auf eine bestimmte Besucherzahl beschränkt. Im Extremfall kann dies auch ein einzelner Besucher sein.
- AutoResetEvent: Ein einzelner Besucher wird durchgelassen.
- ManualResetEvent: Alle dürfen rein, bis ein Wärter sie stoppt.
- Mutex: Nur ein Besucher darf im Gebäude sein.
- Semaphore: Die Besucheranzahl ist beschränkt.
Hinweis |
Durch Speicherung des Zustands werden gesetzte Signale »aufgehoben«. Ein gesetztes Signal kann später von einer Sperrmethode »aufgebraucht« werden, sodass keine Signale verloren gehen. |
Die abstrakte Basisklasse für die zustandsbehafteten Sperren enthält alle wesentlichen Methoden zur signalbasierten Steuerung von Threads. Insbesondere werden die Methoden zum Warten von allen Kindklassen unverändert übernommen. Tabelle 5.5 zeigt die öffentlichen Mitglieder, von denen ich die meisten im Rahmen der konkreten Kindklassen erläutere. Die Variable <timeout> repräsentiert die Alternativen millisecondsTimeout As Integer, timeout As TimeSpan und Timeout.Infinite sowie zusätzlich den Parameter exitContex As Boolean (Aufgabe der Synchronisation während des Aufrufs).
Mitglied | Beschreibung | |
WaitOne(<timeout>) As Boolean |
Warten auf ein Signal sowie Rückgabe, ob das Signal eingetroffen ist |
|
WaitAll(handles As WaitHandle(), <timeout>) As Boolean |
Warten, dass alle Handles ein Signal erhalten mit Rückgabe, ob alle empfangen wurden |
S |
WaitAny(handles As WaitHandle(), <timeout>) As Integer |
Warten, dass ein Handle ein Signal erhält, und Rückgabe des Index des Signalempfängers |
S |
SignalAndWait(to As WaitHandle, from As WaitHandle, <timeout>) As Boolean |
Warten, dass to ein Signal erhält und from ein Signal sendet, sowie Rückgabe, ob beides abgeschlossen wurde (timeout wirkt nur auf das Warten). Die Methode ist atomar. |
S |
Close() |
Aufräumen nichtverwalteter Ressourcen |
|
SafeWaitHandle() As SafeWaitHandle |
Betriebssystemseitige Datenstruktur |
P |
WaitTimeout As Integer |
Konstante für beliebig langes Warten |
C |
Signalgesteuert
Damit jeweils nur ein einzelner Besucher das Gebäude betreten kann, muss nach jedem Besucher das Drehkreuz wieder gesperrt werden. Die passende Klasse hierfür ist AutoResetEvent. Im folgenden Beispiel lässt WaitOne() in der Methode Besucher() eine Person (Thread) warten, bis das Drehkreuz durch Set() in der Methode Verkäufer() freigeschaltet wird (Zustand ist signalisiert). Sofort danach wird das Drehkreuz wieder blockiert. Der Namensbestandteil AutoReset der Klasse weist darauf hin. In Test() werden drei Besucherthreads gestartet und durch drei Aufrufe der Methode Verkäufer() durchgelassen. Zu Beginn ist das Drehkreuz durch den Parameter False von AutoResetEvent gesperrt.
'...\ Multitasking\Datenaustausch\Einzeln.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Einzeln
Private drehkreuz As New AutoResetEvent(False)
Sub Besucher()
drehkreuz.WaitOne()
Console.WriteLine("Einlass von {0}.", Thread.CurrentThread.Name)
End Sub
Sub Verkäufer()
Console.Write("Nächste bitte (Return drücken).") : Console.ReadLine()
drehkreuz.Set()
End Sub
Sub Test()
Dim b As New Thread(AddressOf Besucher) : b.Name = "Booch"
Dim r As New Thread(AddressOf Besucher) : r.Name = "Rumbaugh"
Dim j As New Thread(AddressOf Besucher) : j.Name = "Jacobson"
b.Start() : r.Start() : j.Start()
Verkäufer() : Verkäufer() : Verkäufer()
Console.ReadLine()
End Sub
End Module
End Namespace
Verkäufer und Besucher folgen nicht unbedingt direkt aufeinander. Jeder Freigabe entspricht exakt eine Beanspruchung durch WaitOne(), aber die Verkäuferthreads können so schnell aufeinander folgen, dass sie mehrfach aufgerufen werden, bevor der nächste Besucherthread zum Zuge kommt.
Nächste bitte (Return drücken).
Einlass von Jacobson.
Nächste bitte (Return drücken).
Nächste bitte (Return drücken).Einlass von Booch.
Einlass von Rumbaugh.
In der Analogie des Drehkreuzes können auch mehrere Besucher durchgelassen werden. In der Klassenhierarchie wird dies durch ManualResetEvent ermöglicht. Im Gegensatz zum vorigen Beispiel bleibt im folgenden Beispiel das Drehkreuz so lange offen, bis es durch einen expliziten Aufruf von Reset() in der Methode Besucher() wieder gesperrt wird (Zustand unsignalisiert). Das Beispiel lässt drei Besucher durch.
'...\ Multitasking\Datenaustausch\Gruppe.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Gruppe
Private drehkreuz As New ManualResetEvent(False)
Private Anzahl As Integer
Sub Besucher()
drehkreuz.WaitOne()
Anzahl += 1
If Anzahl = 3 Then drehkreuz.Reset()
Console.WriteLine("Einlass von {0}.", Thread.CurrentThread.Name)
End Sub
Sub Verkäufer()
Anzahl = 0
Console.Write("Nächste bitte (Return drücken).") : Console.ReadLine()
drehkreuz.Set()
End Sub
Sub Test()
For Each n As String In New String() _
{"Silvia", "Sonja", "Henri", "Philip"}
Dim t As New Thread(AddressOf Besucher) : t.Name = n : t.Start()
Next
Verkäufer() : Thread.Sleep(0)
Verkäufer()
Console.ReadLine()
End Sub
End Module
End Namespace
Hier sorgt der Aufruf von Sleep() (mit großer Wahrscheinlichkeit) für ein sauberes Aufeinanderfolgen der verschiedenen Threads.
Nächste bitte (Return drücken).
Einlass von Philip.
Einlass von Silvia.
Einlass von Sonja.
Nächste bitte (Return drücken).
Einlass von Henri.
Einschreiben mit Rückschein
Es gibt Situationen, in denen Sie gewährleisten möchten, dass andere Threads eine Nachricht erhalten, bevor der laufende Thread fortfährt. Im folgenden Beispiel schafft ein Lehrer Signalisierungsmöglichkeiten namens Zettel und startet Schülerthreads mit der Methode Abgabe(). In dieser simuliert Sleep() das Schreiben einer Klausur und Set() deren Abgabe. In der Methode Test() blockiert WaitAll() den Hauptthread, bis alle Schüler mit Set() die Abgabe der Klausur signalisiert haben.
'...\ Multitasking\Datenaustausch\Lehrer.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Lehrer
Dim Schüler As String() = _
{"Xenophon", "Alkibiades", "Antisthenes", "Aristipp", "Aischines"}
Private Zettel(4) As AutoResetEvent
Sub Abgabe(ByVal no As Object)
Console.Write("{0} ", Schüler(CType(no, Integer)))
Thread.Sleep(2000)
Zettel(CType(no, Integer)).Set()
End Sub
Sub Test()
For no As Integer = 0 To Schüler.Length – 1
Zettel(no) = New AutoResetEvent(False)
Dim t As New Thread(AddressOf Abgabe) : t.Start(no)
Next
WaitHandle.WaitAll(Zettel)
Console.WriteLine(Environment.NewLine & "Auf Wiedersehen.")
Console.ReadLine()
End Sub
End Module
End Namespace
Erst nach Abschluss der Signalisierung kann der Hauptthread abschließen.
Alkibiades Xenophon Antisthenes Aristipp Aischines
Auf Wiedersehen.
Systemweite Sperre
Ein großer Vorteil der WaitHandle-Klassen liegt in der Möglichkeit, dieselbe Sperre in verschiedenen Anwendungen (Prozessen) zu verwenden. Dies erreichen Sie dadurch, dass Sie einen Namen für die Sperre vergeben. Er erlaubt den Zugriff von einem beliebigen Prozess aus, auch außerhalb der .NET-Umgebung.
Hinweis |
Namenslose WaitHandle ("" oder Nothing) sind auf eine Applikation beschränkt. |
Hinweis |
In den Terminal Services ist ein WaitHandle, dessen Name mit »Global\« beginnt, in allen Sessions sichtbar. Auf eine Session begrenzte Handles starten mit »Local\«. |
Das folgende Beispiel startet eine Anwendung zweimal. Dadurch entstehen zwei völlig unabhängige Prozesse, die nicht direkt aufeinander zugreifen können. Das WaitHandle schalter wird durch den Namen "Taste" systemweit bekannt gemacht. Durch den Startzustand signalisiert wird beim ersten Start der Aufruf von WaitOne() in der Methode Test() nicht blockiert, und der True-Zweig wird ausgeführt. Dieser startet die Anwendung ein zweites Mal und danach die Methode Uhr().Die relativ komplizierte Art des Starts – Kopie der Anwendung und Start der Kopie – erlaubt auch einen Start über das Netzwerk, ohne dass es zu einer Ausnahme durch eine Sicherheitsüberprüfung kommt. Sie können auf die Zeilen verzichten, wenn Sie die Anwendung manuell zweimal starten. In der Methode Uhr() warten zwei Aufrufe von WaitOne() auf je eine Signalisierung durch einen Aufruf von Set() im Else-Zweig der Anwendung. Der zweite Start der Anwendung führt diesen Zweig aus.
'...\ Multitasking\Datenaustausch\Stoppuhr.vb |
Option Strict On
Imports System.Threading
Imports System.IO
Namespace Multitasking
Module Stoppuhr
Private schalter As New EventWaitHandle( _
True, EventResetMode.AutoReset, "Taste")
Sub Uhr()
schalter.WaitOne()
Dim start As Date = Now
schalter.WaitOne()
Console.WriteLine("Zeitspanne: {0}.", Now.Subtract(start))
End Sub
Sub Test()
If schalter.WaitOne(0, True) Then
'Kopie erlaubt auch exe im Netzwerk (sonst SecurityException)
Dim f As String = Path.Combine(Path.GetTempPath(), "Stopp.exe")
Dim l As String = f.GetType().Assembly.GetCallingAssembly().Location
If File.Exists(f) Then : File.Delete(f) : End If : File.Copy(l, f)
Process.Start(f)
Uhr()
Else
Console.Write("Zeilenvorschub zum Start.") : Console.ReadLine()
schalter.Set()
Console.Write("Zeilenvorschub zum Stopp.") : Console.ReadLine()
schalter.Set()
End If
Console.ReadLine()
End Sub
End Module
End Namespace
Der zweite Prozess (beim zweiten Start) gibt die Aufforderung zur Eingabe.
Zeilenvorschub zum Start.
Zeilenvorschub zum Stopp.
Der erste Prozess (beim ersten Start) gibt die Zeitspanne aus.
Zeitspanne: 00:00:03.2646944.
Die Klasse EventWaitHandle
Tabelle 5.6 fasst die Klasse WaitHandle zusammen.
Methode | Beschreibung | |
New(initial As Boolean, mode As EventResetMode, name As String, ByRef createdNew As Boolean, security As EventWaitHandleSecurity) |
Konstruktor mit Angabe, ob das Objekt signalisiert startet, der Art der Signalrücksetzung, eines systemweiten Namens, mit Rückgabe ob das Objekt neu erzeugt wurde und Angabe von Sicherheitsinformationen. |
|
Set() As Boolean |
Zustand signalisiert und Erfolgsmeldung. |
|
Reset() As Boolean |
Zustand unsignalisiert mit Erfolgsmeldung. |
|
OpenExisting(name As String, rights As EventWaitHandleRights) As EventWaitHandle |
Bezug auf eine betriebssystemweite Sperre (keine Referenz). |
S |
GetAccessControl() As E SetAccessControl(security As E) |
betriebssystemseitige Sicherheitskontrolle E: EventWaitHandleSecurity |
Hinweis |
Die Klassen EventWaitHandle, AutoResetEvent und ManualResetEvent unterscheiden sich ausschließlich durch den Konstruktor. |
Exklusivität mit Mutex
Wollen Sie einen Zugriff auf Daten exklusiv gestalten, bietet sich die Klasse Mutex an. Der Name ist eine Abkürzung für mutual exclusive, übersetzt heißt das »gegenseitig ausschließend«. Es kann immer nur ein Thread gleichzeitig einen Mutex beanspruchen. Alle anderen Threads müssen sich gedulden, bis er wieder freigegeben wird.
Hinweis |
Ohne weitere Angabe wird ein Mutex von keinem Thread beansprucht. |
Das folgende Beispiel nutzt den exklusiven Zugriff auf einen Mutex, um eine Anwendung nur einmal starten zu dürfen. Durch Vergabe eines Namens wird die Sperre betriebssystemweit gültig und kann von den verschiedenen Prozessen genutzt werden, die durch die verschiedenen Starts erzeugt wurden. Wie beim letzten Beispiel wird beim ersten Start die Anwendung ein weiteres Mal gestartet, und wie dort können Sie auf die Zeilen bis inklusive Process.Start() verzichten, wenn Sie die Anwendung manuell ein zweites Mal starten. Alle weiteren Starts landen im Else-Zweig, da der Mutex mittels WaitOne() vom ersten Start beansprucht wird (der Mutex wird durch False nicht vom Hauptthread beansprucht).
'...\ Multitasking\Datenaustausch\Einmalig.vb |
Option Strict On
Imports System.Threading
Imports System.IO
Namespace Multitasking
Module Einmalig
Private sperre As New Mutex(False, "Einmalig")
Sub Test()
If sperre.WaitOne(0, True) Then
'Kopie erlaubt auch exe im Netzwerk (sonst SecurityException)
Dim f As String = Path.Combine(Path.GetTempPath(), "Eins.exe")
Dim l As String = f.GetType().Assembly.GetCallingAssembly().Location
If File.Exists(f) Then : File.Delete(f) : End If : File.Copy(l, f)
Process.Start(f)
Console.Write("Applikation gestartet.")
Else
Console.Write("Nur ein Start erlaubt.")
End If
Console.ReadLine()
End Sub
End Module
End Namespace
Im ersten Konsolenfenster wird die Anwendung ganz normal gestartet:
Applikation gestartet.
Der zweite Start wird zurückgewiesen:
Nur ein Start erlaubt.
Die Sperrmöglichkeiten können leicht zu der Situation führen, dass ein Thread beendet wird, ohne den Mutex vorher zurückzugeben. Der nächste Thread, der dann den Mutex beansprucht, wird mit einer Ausnahme abgestraft. Auf den ersten Blick ist dies unangenehm, aber bei näherer Betrachtung ist dieses Vorgehen sehr vorteilhaft. Oft zeigt nämlich das Threadende ohne Rückgabe eines Mutex einen schwerwiegenden Fehler an, auf den eine Anwendung auch besonders eingehen sollte. Das folgende Beispiel startet in der Methode Test() zwei Threads, die in der Methode Instrument() mit WaitOne() den Mutex beanspruchen. Der Aufruf ReleaseMutex() ist absichtlich auskommentiert, um die Ausnahme AbandonedMutexException zu provozieren.
'...\ Multitasking\Datenaustausch\Einmalig.vb |
Option Strict On
Imports System.Threading
Imports System.IO
Namespace Multitasking
Module Vergessen
Private dir As New Mutex(False, "Dirigent")
Sub Instrument()
Try
dir.WaitOne()
Catch ex As AbandonedMutexException
Console.WriteLine("{0}: Der Komponist hat geschlafen.", _
Thread.CurrentThread.Name)
End Try
Console.WriteLine("{0} spielt.", Thread.CurrentThread.Name)
'dir.ReleaseMutex()
End Sub
Sub Test()
Dim geige As New Thread(AddressOf Instrument) : geige.Name = "Geige"
Dim cello As New Thread(AddressOf Instrument) : cello.Name = "Cello"
geige.Start()
cello.Start()
Console.ReadLine()
End Sub
End Module
End Namespace
Das zweite Instrument muss auf die Freigabe durch das erste warten. Die passiert indirekt durch das Ende des Threads. Dadurch wird im zweiten Thread eine Ausnahme ausgelöst.
Geige spielt.
Cello: Der Komponist hat geschlafen.
Cello spielt.
Um diese Situation zu vermeiden, bietet es sich an, die Rückgabe eines Mutex grundsätzlich in einen Finally-Zweig zu packen, damit er auch im Fehlerfall zurückgegeben wird.
Try |
Obwohl Mutex die Schnittstelle IDisposable implementiert, ist eine elegante Rückgabe des Mutex nach folgendem Muster nicht möglich, da Dispose() die Methode ReleaseMutex() nicht aufruft, sondern nur unverwaltete Ressourcen zurückgibt.
'gibt Mutex nicht zurück
Using dir As Mutex = Mutex.OpenExisting("Dirigent")
dir.WaitOne()
Console.WriteLine("{0} spielt.", Thread.CurrentThread.Name)
End Using
Tabelle 5.7 fasst die Klasse Mutex zusammen.
Methode | Beschreibung | |
New(initial As Boolean, name As String, ByRef createdNew As Boolean, security As MutexSecurity) |
Konstruktor mit Angabe, ob das Objekt vom Erzeuger direkt beansprucht wird, eines systemweiten Namens, Rückgabe ob das Objekt neu erzeugt wurde und Angabe von Sicherheitsinformationen. |
|
ReleaseMutex() |
Rückgabe des Mutex |
|
OpenExisting(name As String, rights As MutexRights) As Mutex |
Bezug auf eine betriebssystemweite Sperre (keine Referenz) |
S |
GetAccessControl() As M SetAccessControl(security As M) |
betriebssystemseitige Sicherheitskontrolle M: MutexSecurity |
Ressourcenbegrenzung mit Semaphore
Wenn eine Ressource nicht exklusiv gesperrt werden soll, sondern nur ihre Nutzung nicht unbeschränkt sein soll, kommt die Klasse Semaphore (Signalmast) ins Spiel. In einem internen Zähler hält ein solches Objekt die Anzahl Threads fest, die es beanspruchen. Das folgende Beispiel nutzt die Bewegungen auf einem Konto, dessen Guthaben nach oben beschränkt ist. In der Methode Einzahlung() wird der Semaphor durch den Befehl Release() freigegeben, damit er in Auszahlung durch WaitOne() in Anspruch genommen werden kann. Der Zugriff auf das Konto erfolgt über dessen Name Konto mittels der Methode OpenExisting(). Es wird an einer anderen Stelle erzeugt, gegebenenfalls sogar durch ein anderes Programm. In der Methode Auszahlung() blockiert die Methode WaitOne(), wenn keine Einzahlung erfolgt ist, und verhindert so eine Unterdeckung des Kontos. Eine Überschreitung des im Semaphor Konto erlaubten maximalen Guthabens wird nicht kontrolliert und kann zu einer Ausnahme führen (siehe unten).
'...\ Multitasking\Datenaustausch\Buchung.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Class Buchung
Private Shared rnd As New Random(3)
Private Anzahl As Integer
Sub Einzahlung()
Dim konto As Semaphore = Semaphore.OpenExisting("Konto")
Anzahl = rnd.Next(2, 6)
konto.Release(Anzahl)
Console.WriteLine("{0} eingezahlt.", Anzahl)
End Sub
Sub Auszahlung()
Dim konto As Semaphore = Semaphore.OpenExisting("Konto")
Anzahl = -rnd.Next(2, 6)
For no As Integer = 1 To -Anzahl : konto.WaitOne() : Next
Console.WriteLine("{0} abgehoben.", -Anzahl)
End Sub
...
End Module
End Namespace
In der Methode Test() wird als Erstes der Semaphor Konto erzeugt. Durch die Vergabe eines Namens ist er betriebssystemweit bekannt und kann in den bereits gezeigten Methoden Einzahlung() und Auszahlung() über seinen Namen angesprochen werden. Das maximale Guthaben wird auf fünf festgelegt, und das Konto startet ohne Guthaben. In der ersten Schleife werden einige Threads gestartet, die auf das Konto einzahlen und davon abheben. Damit der Hauptthread das Programm auch bei noch ausstehenden Buchungen beenden kann, sind alle Buchungsthreads als Hintergrundthreads definiert.
Bei den Kontenbewegungen kann es sein, dass das maximal erlaubte Guthaben überschritten wird und im betreffenden Thread eine Ausnahme ausgelöst wird. Da dieser unabhängig von anderen läuft, gibt es keine geeignete Stelle für einen Try/Catch-Block zum Auffangen der Ausnahme. Damit sie das Programm nicht mit in den Abgrund reißt, wird in Test() die Methode un() als Ereignishandler nichtbehandelter Ausnahmen registriert. Der Handler wird informiert, hat aber keinen Einfluss auf die Fehlerbehandlung und kann die Ausnahme nicht verhindern. Der Einfachheit halber hält der Thread sich selbst mit Suspend() an, damit die Ausnahme nach Beendigung des Handlers das Programm nicht abbricht.
'...\ Multitasking\Datenaustausch\Buchung.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Class Buchung
...
Sub un(ByVal sender As Object, ByVal ev As UnhandledExceptionEventArgs)
Console.WriteLine( _
"Thread {0} versucht {1} zu buchen (Ausnahme {2}).", _
Thread.CurrentThread.Name, Anzahl, _
ev.ExceptionObject.GetType().Name)
Thread.CurrentThread.Suspend() ' => Beispiel kurz
End Sub
Private Shared t(9) As Thread
Sub start(ByVal addr As ThreadStart, ByVal nr As Integer)
t(nr) = New Thread(addr)
t(nr).IsBackground = True : t(nr).Name = nr.ToString()
t(nr).Start()
End Sub
Shared Sub Test()
Dim konto As New Semaphore(0, 5, "Konto"), b As Buchung
For no As Integer = 0 To 9
b = New Buchung()
AddHandler AppDomain.CurrentDomain.UnhandledException, _
AddressOf b.un
If (no Mod 2) = 0 Then b.start(AddressOf b.Einzahlung, no) _
Else b.start(AddressOf b.Auszahlung, no)
Next
Thread.Sleep(2000)
For Each th As Thread In t
Console.Write("{0}:{1} ", th.Name, th.ThreadState)
Next
Console.ReadLine()
End Sub
End Module
End Namespace
Der vierte Buchungsthread überschreitet das erlaubte Limit und löst eine Ausnahme aus. In den letzten beiden Zeilen ist der Zustand des angehaltenen Threads auch zu erkennen. Der letzte Thread ist auch noch nicht abgeschlossen; WaitOne() in der Methode Auszahlung() wartet noch auf eine ausreichende Einzahlung.
3 eingezahlt.
4 abgehoben.
5 eingezahlt.
2 abgehoben.
2 abgehoben.
3 eingezahlt.
Thread 4 versucht 3 zu buchen (Ausnahme SemaphoreFullException).
5 abgehoben.
3 eingezahlt.
0:Stopped 1:Stopped 2:Stopped 3:Stopped 4:Background, Suspended 5:Stopped
6:Stopped 7:Stopped 8:Stopped 9:Background, WaitSleepJoin
Tabelle 5.8 fasst die Klasse Semaphore zusammen.
Methode | Beschreibung | |
New(initial As Integer, max As Integer, name As String, ByRef createdNew As Boolean, security As SemaphoreSecurity) |
Konstruktor mit Vorbelegung durch Erzeuger, der Maximalbelegung, eines systemweiten Namens, Rückgabe ob das Objekt neu erzeugt wurde und Angabe von Sicherheitsinformationen. |
|
Release(count As Integer) As Integer |
Ein- oder mehrmalige Rückgabe des Semaphors und Rückgabe der bisherigen Belegung |
|
OpenExisting(name As String, rights As SemaphoreRights) As Semaphore |
Bezug auf eine betriebssystemweite Sperre (keine Referenz) |
S |
GetAccessControl() As S SetAccessControl(security As S) |
Betriebssystemseitige Sicherheitskontrolle. M: SemaphoreSecurity |
5.3.4 Atomare Operationen 

Die meisten Operationen können durch einen Threadwechsel des Betriebssystems unterbrochen werden. Selbst für eine so einfache Operation wie das Auslesen eines Wertes kann keine Garantie gegeben werden, dass sie nicht unterbrochen wird. Es hängt von der CPU ab, welche Befehle atomar und damit nicht unterbrechbar sind. Im Zweifellsfall sollten Sie immer von der Möglichkeit einer Unterbrechung ausgehen und die atomaren Methoden der Klasse Interlock zum Zugriff verwenden (siehe Tabelle 5.9).
Methode | Beschreibung |
Read(var As Long) |
Wert auslesen |
Increment(var As z) As z Decrement(var As z) As z |
Änderung von var um eins und Rückgabe. z: Integer oder Long |
Add(var As z, value As z) As z |
var=value und Rückgabe vom neuen var. z: Integer oder Long |
Exchange(var As z, value As z) As z |
var=value und Rückgabe des alten var. z: Integer, Long, Single, Double, Object, IntPtr, T |
CompareExchange(var As z, value As z, comparand As z) As z |
Wenn var=comparand, dann var=value. z: Integer, Long, Single, Double, Object, IntPtr, T |
5.3.5 Attributgesteuerte Synchronisation 

In einigen Situationen ist die Ausprogrammierung einer Synchronisation recht aufwendig. Sind die Zusammenhänge einfach, gibt es einige Attribute, die durch reine Deklaration die Daten bei konkurrierenden Zugriffen schützen. Ich greife hier zwei heraus.
ThreadStatic
Können Sie threadabhängige Zustände in einer klassengebundenen Variablen speichern, sorgt das Attribut ThreadStatic dafür, dass jeder Thread trotz gleichen Namens einen anderen Speicherplatz für die Variable nutzt. Dadurch kann es gar nicht zu Konflikten kommen, da keine Konkurrenz entsteht. Außerdem sind sie recht performant im Vergleich zu einigen anderen Synchronisationsmechanismen. Das folgende Beispiel ändert die klassengebundene Variable wert in sechs verschiedenen Threads und gibt sie aus. Danach wird jeder der Threads mit WaitOne() angehalten. Nach einer Pause, die allen Zahlenthreads genug Zeit zur Änderung gibt, werden die angehaltenen Threads durch den Hauptthread mit Set() wieder freigegeben.
'...\ Multitasking\Datenaustausch\Neu.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Neu
Private sperre As New ManualResetEvent(False)
Private generator As New Random()
<ThreadStatic()> Private wert As Integer
Sub zahl(ByVal nr As Object)
wert = generator.Next(100, 1000)
Console.Write("{0}:{1} ", nr, wert)
sperre.WaitOne()
Console.Write("{0}:{1} ", nr, wert)
End Sub
Sub Test()
For nr As Integer = 0 To 5
Dim t As New Thread(AddressOf zahl) : t.Start(nr)
Next
Thread.Sleep(1000)
Console.WriteLine(Environment.NewLine & "Hauptthread: {0}", wert)
Thread.Sleep(1000)
sperre.Set()
Console.ReadLine()
End Sub
End Module
End Namespace
Die Reihenfolge der Threads ist nicht garantiert, dafür aber die Eindeutigkeit der Daten: Jeder Thread verwendet eine eigene Version der klassengebundenen Variablen.
0:380 1:298 2:740 3:990 4:303 5:984
Hauptthread: 0
5:984 0:380 1:298 2:740 3:990 4:303
Synchronisation
Wenn Sie eine Klasse von ContextBoundObject ableiten, können Sie die Synchronisation aller Methoden und Eigenschaften mit dem Attribut Synchronization im Namensraum System.Runtime.Remoting.Contexts auf einen Schlag erledigen. Im folgenden Beispiel wird die Klasse Zahl von ContextBoundObject abgeleitet und in der wertverändernden Eigenschaft Wert eine künstliche Verzögerung eingebaut. In der Methode Test() wird im ersten Thread der Wert in der Methode setzen() verändert und nach einer kurzen Pause (kürzer als die eingebaute Verzögerung) der Wert in der Methode lesen() abgefragt. Durch die verschieden langen Verzögerungen kommt es zu einem konkurrierenden Zugriff.
'...\ Multitasking\Datenaustausch\Kontext.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
<System.Runtime.Remoting.Contexts.Synchronization()> Class Zahl
Inherits ContextBoundObject
Public w As Long
WriteOnly Property Wert() As Long
Set(ByVal value As Long)
Thread.Sleep(1000) : w = value
End Set
End Property
End Class
Module Kontext
Dim wert As New Zahl()
Sub setzen()
wert.Wert = 777
End Sub
Sub lesen()
Console.Write("Wert {0}", wert.w)
End Sub
Sub Test()
Dim s As New Thread(AddressOf setzen) : s.Start() : Thread.Sleep(100)
Dim l As New Thread(AddressOf lesen) : l.Start()
Console.ReadLine()
End Sub
End Module
End Namespace
Das Auslesen findet nach der Zuweisung statt.
Wert 777
MethodImpl
Wenn eine ganze Methode nur von einem Thread benutzt werden darf, können Sie, anstatt den ganzen Rumpf mit Monitor-Aufrufen zu kapseln, der Methode das Attribut MethodImpl aus dem Namensraum System.Runtime.CompilerServices geben. Das Attribut spezifiziert die Art der Implementierung einer Methode durch einen Parameter vom Typ der Enumeration MethodImplOptions, deren Werte Tabelle 5.10 zeigt.
Konstante | Beschreibung |
Unmanaged |
Die Methode ist in unverwaltetem Code implementiert. |
ForwardRef |
Reine Methodendeklaration (Implementierung steht woanders) |
PreserveSig |
Die Methodensignatur wird exakt wie angegeben exportiert. |
InternalCall |
Die Methode, die direkt in der CLR implementiert ist |
Synchronized |
Nur ein Thread gleichzeitig darf die Methode ausführen. |
NoInlining |
Die Methode darf nicht durch Optimierung zu Inline-Code werden. |
NoOptimization |
Es sind keine Optimierungen erlaubt (kann das Debuggen erleichtern). |
Von diesen Werten brauchen wir nur Synchronized. Im folgenden Beispiel ist in der Methode unterschreiben künstlich eine Verzögerung eingebaut, um anderen Threads Zeit zum Eingreifen zu geben.
'...\ Multitasking\Datenaustausch\Implementierung.vb |
Option Strict On
Imports System.Threading
Imports System.Runtime.CompilerServices
Namespace Multitasking
Module Implementierung
Dim urteil As String = "/ "
<MethodImpl(MethodImplOptions.Synchronized)> _
Sub unterschreiben()
urteil += "Richter "
Thread.Sleep(1000)
urteil += "Adam / "
End Sub
Sub anfrage()
unterschreiben()
End Sub
Sub Test()
Dim z1 As New Thread(AddressOf anfrage) : z1.Start()
Dim z2 As New Thread(AddressOf anfrage) : z2.Start()
While urteil.Length < 32 : Thread.Sleep(100) : End While
Console.WriteLine("Unterschriften: {0}", urteil)
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe zeigt die erfolgreiche Synchronisierung.
Unterschriften: / Richter Adam / Richter Adam /
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.