Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 <<   zurück
Visual Basic 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual Basic 2005

Visual Basic 2005
1.233 S., mit 2 CDs, 59,90 Euro
Rheinwerk Computing
ISBN 3-89842-585-1
gp Kapitel 11 Multithreading und asynchrone Methodenaufrufe
  gp 11.1 Prozesse und Threads
    gp 11.1.1 Multithreading
    gp 11.1.2 Thread-Zustände und Prioritäten
    gp 11.1.3 Einsatz von mehreren Threads
  gp 11.2 Die Entwicklung einer Multithreading-Anwendung
    gp 11.2.1 Die Klasse »Thread«
    gp 11.2.2 Threadpools nutzen
  gp 11.3 Die Synchronisation von Threads
    gp 11.3.1 Unsynchronisierte Threads
    gp 11.3.2 Der »Monitor« zur Synchronisation
    gp 11.3.3 Das Synchronisationsobjekt »Mutex«
    gp 11.3.4 Das Attribut »MethodImpl«
  gp 11.4 Asynchrone Methodenaufrufe
    gp 11.4.1 Eine kleine Einführung
    gp 11.4.2 Asynchroner Methodenaufruf
    gp 11.4.3 Asynchroner Aufruf mit Rückgabewerten
    gp 11.4.4 Eine Klasse mit asynchronen Methodenaufrufen


Galileo Computing

11.3 Die Synchronisation von Threads  downtop

Solange nur ein Thread eine bestimmte Methode aufruft, hat man die Garantie, dass der Code von der ersten bis zur letzten Anweisung durchlaufen wird. Sind mehrere Threads im Spiel, könnte einer der Threads eine Methode in einem ungültigen Zustand hinterlassen, wenn das System ihm die Zeitscheibe quasi mitten in der Ausführung der Methode entzieht und der nächste Thread mit derselben Methode auf demselben Objekt zu arbeiten beginnt. Der Thread, der den Objektzustand von seinem Vorgänger übernommen hat, produziert dann möglicherweise Ergebnisse, die nicht vorhersehbar und in der Regel auch falsch sind.

Hier kommt ein neuer Begriff ins Spiel, der Ihnen vielfach in der Dokumentation zur .NET-Klassenbibliothek vor Augen kommt: die Thread-Sicherheit.


Unter Thread-Sicherheit wird verstanden, dass ein Objekt auch dann in einem gültigen Zustand bleibt, wenn mehrere Threads gleichzeitig auf dieselbe Ressource zugreifen.


Threadsicher bedeutet nichts anderes, als dass mehrere Threads gleichzeitig dieselbe Methode desselben Objekts aufrufen dürfen, ohne dass es zu Konflikten kommt.


Galileo Computing

11.3.1 Unsynchronisierte Threads  downtop

Bevor wir uns mit den Details der Thread-Sicherheit beschäftigen, wollen wir uns an einem Beispiel verdeutlichen, was unter einem ungültigen Zustand zu verstehen ist und welche Auswirkungen das haben kann.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 11\UnsynchronisierteThreads
' ----------------------------------------------------------
Imports System.Threading
Module Module1
Sub Main()
Dim obj As New ClassA
Dim thread1, thread2 As Thread
thread1 = New Thread(New ThreadStart(AddressOf obj.Worker))
thread2 = New Thread(New ThreadStart(AddressOf obj.Worker))
thread1.Start()
thread2.Start()
Console.ReadLine()
End Sub
End Module
Class ClassA
Private intVar As Integer
Public Sub Worker()
Do While (True)
intVar += 1
If (intVar > 100) Then Exit Do
Console.WriteLine(intVar)
Loop
End Sub
End Class

Das Projekt enthält zusätzlich zu Main noch die Definition der Methode Worker in ClassA. In Main werden zwei Threads konstruiert, die beide die Methode Worker aufrufen. Worker selbst durchläuft eine Schleife, in der die Variable intVar hochgezählt und der aktuelle Inhalt an der Konsole ausgegeben wird. Mit dem Endwert von 100 wird die Methode wieder verlassen.

Beide Threads greifen auf dasselbe Objekt zu und teilen sich die Arbeit mehr oder weniger abwechselnd, um das Feld intVar hochzuzählen und dessen Inhalt anzuzeigen. Eigentlich sollte man erwarten, dass die Zahlen chronologisch hintereinander ausgegeben werden, jedoch kommt es an der Konsole beispielsweise zu folgender Ausgabe:


1
2
3
4
5
6
7
....
39
41
42
43
...
99
100
40

Beide Threads greifen unsynchronisiert auf die Variable intVar zu, wobei die Operation des ersten Threads mitten in der Schleife unterbrochen wird. Dieses ist dem Anschein nach genau der Moment, nachdem der Feldinhalt mit der Anweisung


intVar += 1

zwar schon auf 40 erhöht, aber mit


Console.WriteLine(intVar)

noch nicht an der Konsole ausgegeben wurde. Der unterbrochene Thread weiß natürlich genau, mit welcher Anweisung er seine Arbeit wieder aufnehmen muss, wenn ihm der Scheduler wieder Prozessorzeit zuteilt: Er muss zuerst die Zahl 40 ausgeben. Diesen Zwischenstand, dessen Informationen durch den Inhalt der CPU-Register beschrieben werden, speichert das System im Stack und räumt daraufhin den Prozessor für den nächsten Thread in der Warteschlange.

Der zweite Thread, dem anschließend die CPU zugeteilt wird, tritt nun seinerseits zum ersten Mal in die Schleife ein, erkennt den aktuell gültigen Feldinhalt der Variablen (er beträgt 40), erhöht diesen zunächst auf 41, gibt den Wert aus und setzt die Schleife so lange fort, bis seine Zeit abgelaufen ist. Dann verlässt der zweite Thread die CPU, das System liest die im Stack gesicherten Daten des ersten Threads in die CPU ein und setzt die Arbeit mit genau der Anweisung fort, bei der er unterbrochen wurde: die Ausgabe der Zahl 40 an der Konsole.


Galileo Computing

11.3.2 Der »Monitor« zur Synchronisation  downtop

Beide Threads des Beispiels arbeiten ohne Synchronisation und hinterlassen ihrem Nachfolger die Ressource in einem ungültigen Zustand. Das wollen wir natürlich vermeiden – die Feldinhalte sollen so ausgegeben werden, dass sie dem tatsächlich aktuellen Stand des Feldes entsprechen.

Wenn wir die Arbeitsweise der Methode Worker analysieren, kommen wir zu der Feststellung, dass ein ganz bestimmter Codeteil als kritisch angesehen werden kann. Es sind die beiden Anweisungen


intVar += 1
Console.WriteLine(intVar)

Die Ausgabe wird nur dann unseren Erwartungen entsprechen, wenn ein laufender Thread seine Ausführung nicht zwischen diesen beiden Anweisungen unterbrechen muss, denn zur Erhöhung des Feldwertes gehört auch die Anzeige an der Konsole. Dieser Zusammenhang muss für jeden der beiden Threads ersichtlich sein.


Hinweis

Einige Operationen können auch bei einem bevorstehenden Wechsel der Zeitscheibe nicht unterbrochen werden. Solche Operationen werden als atomare Operationen bezeichnet.

Dazu gehören beispielsweise einfache Schreib- und Lesevorgänge auf Integerzahlen, während die gleichen Vorgänge auf Dezimalzahlen nicht atomar sind. Auch Operationen mit den Operatoren += und –= sind nicht atomar. Deshalb kann es auch zu einem Zeitscheibenwechsel mitten in der Ausführung von intVar += 1 kommen.


An dieser Stelle kommt eine neue Klasse ins Spiel, welche die Aufgabe einer Synchronisation übernimmt: Monitor. Mit dieser Klasse lässt sich verhindern, dass mehrere Threads gleichzeitig einen bestimmten Codeteil im Programm durchlaufen. Mit anderen Worten bedeutet das, dass zu einem Zeitpunkt immer nur ein Thread dieses Codesegment durchlaufen kann. Andere Threads, die ebenfalls dieses Codesegment ausführen wollen, müssen warten, bis der laufende Thread das Codesegment verlassen hat.

Mit den Methoden Enter und Exit der Klasse Monitor können kritische Codeabschnitte definiert werden, die zu einem gegebenen Zeitpunkt nur von einem Thread betreten werden dürfen. Mit Enter wird das Codesegment so lange blockiert, bis die Sperrung mit Exit wieder aufgehoben wird. Damit sind ungültige Zustände, die ein Thread hinterlassen könnte, wenn ihm die Zeitscheibe entzogen wird, nicht mehr möglich. Monitor protokolliert, ob der Vorgänger-Thread den kritischen Abschnitt mit Exit ordnungsgemäß verlassen hat oder nicht.

Sowohl Enter als auch Exit sind statische Methoden der Klasse Monitor. Als Argument wird den beiden Methoden die Referenz auf das zu synchronisierende Objekt übergeben, das auch Me sein darf.


Public Shared Sub Enter(obj As Object)
Public Shared Sub Exit(obj As Object)

Wir ändern jetzt das Beispiel oben und schaffen die Voraussetzung, dass die Zugriffe auf die kritischen Anweisungen synchronisiert erfolgen. Zur Bestätigung lassen wir uns diesmal zusätzlich noch den Hashcode des jeweiligen Threads ausgeben, der die angezeigte Zahl erzeugt hat.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 11\SynchronisierteThreads
' ----------------------------------------------------------...
Class ClassA
Private intVar As Integer
Public Sub Worker()
Do While (True)
' Sperre setzen
Monitor.Enter(Me)
intVar += 1
If (intVar > 100) Then Exit Do
Console.WriteLine("Zahl = {0,5} Thread = {1,3}", intVar, _
Thread.CurrentThread.GetHashCode().ToString())
Thread.Sleep(5)
' Sperre aufheben
Monitor.Exit(Me)
Loop
End Sub
End Class

Nun erhalten wir wunschgemäß die Ausgabe der chronologisch geordneten Zahlen von 1 bis 100.

Neben der Enter-Methode gibt es in der Monitor-Klasse noch die Methode TryEnter. Diese überprüft zuerst, ob der geschützte Codeabschnitt frei ist, sperrt ihn dann und führt den Code aus. Ist das Codesegment gesperrt, liefert TryEnter den Rückgabewert False. Darauf kann der Entwickler entsprechend reagieren. Der Rückgabewert kann beispielsweise von einem If-Statement sinnvoll ausgewertet werden.

Das »SyncLock«-Statement

Neben Enter und Exit der Klasse Monitor gibt es noch eine andere, sprachspezifische Möglichkeit, den Zugriff zu synchronisieren. Unter Visual Basic ist das die SyncLock-Anweisung. Die Syntax dazu lautet:


SyncLock <Ausdruck>
'Anweisungen
End SyncLock

Durch den Anweisungsblock hinter SyncLock werden die Anweisungen eingeschlossen, die es zu synchronisieren gilt. Dieses Statement ist sehr einfach zu handhaben, aber es besitzt nicht die Möglichkeiten, mit denen die Klasse Monitor ausgestattet ist.

Synchronisierte und unsynchronisierte Methoden in einer Klasse

Wir wollen im folgenden Beispiel lock einsetzen und dabei gleichzeitig die Frage beantworten, wie sich die Synchronisation einer Methode auf die anderen Methoden desselben Objekts auswirkt, die nicht synchronisiert sind. Dazu entwickeln wir eine Klasse ClassA mit den Methoden SyncProc und UnsyncProc. Wie der Bezeichner schon zum Ausdruck bringt, unterstützt nur erstere den synchronisierten Zugriff.


' ----------------------------------------------------------
' Beispiel: ...\Beispielcode\Kapitel 11\SyncLock
' ----------------------------------------------------------
Imports System.Threading
Module Module1
Sub Main()
Dim obj1 As New ClassA()
Dim obj2 As New ClassA()
Dim Thread1, Thread2 As Thread
'Threads instanziieren
Thread1 = New Thread(AddressOf obj1.SyncProc)
Thread2 = New Thread(AddressOf obj1.UnsyncProc)
'Threads starten
Thread2.Start()
Thread1.Start()
Console.ReadLine()
End Sub
End Module
Public Class ClassA
'synchronisierte Methode
Public Sub SyncProc()
Dim i As Int32
SyncLock Me
For i = 0 To 100
Thread.Sleep(20)
Console.Write(".{0}.", i)
Next
End SyncLock
End Sub
'unsynchronisierte Methode
Public Sub UnsyncProc()
Dim i As Int32
For i = 0 To 50
Console.Write("..STOP..")
Thread.Sleep(20)
Next
End Sub
End Class
Die folgende Abbildung zeigt die Ausgabe an der Konsole.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 11.8     Ausgabe des Beispiels »Lock-Anweisung«

In SyncProc wird in einer Schleife der Schleifenzähler nach dem Aufruf der Sleep-Methode an der Konsole ausgegeben, in UnsyncProc die ebenfalls zeitverzögerte Zeichenfolge ..STOP.. –. Würde sich die Synchronisation auf alle Threads auswirken, die sich der Methoden desselben Objekts bedienen, müsste der Zähler von 0 bis 100 ununterbrochen hintereinander an der Konsole erscheinen.

Tatsächlich ist es aber so, dass sich die unsynchronisierte Methode völlig unbeeindruckt davon zeigt, dass ein anderer Thread auf die synchronisierte zugreift – die Synchronisation bezieht sich nur auf die Anweisungen innerhalb des zur Synchronisation gekennzeichneten Blocks.

Mehrere synchronisierte Codeabschnitte in einem Objekt

Vielleicht haben Sie sich schon die Frage gestellt, warum sich der Ein- und der Austrittsanweisung in den Monitor eine Objektreferenz anschließt, beispielsweise:


Monitor.Enter(Me)

Das legt die Vermutung nahe, dass es zu einem gegebenen Objekt auch nur einen Monitor gibt, oder mit anderen Worten: Es kann auf ein Objekt nur eine Sperre gelegt werden. Der Code im vorherigen Beispiel fordert geradezu heraus, entsprechend geändert zu werden, um diese Vermutung bestätigt zu sehen. Wir ändern dazu zunächst den Namen der Prozeduren ab – denn auf eine Methode mit dem Namen UnsyncProc eine Synchronisation erzwingen zu wollen, würde nur zu Missverständnissen führen. In der umbenannten Methode fügen wir dann noch den Monitor hinzu (es könnte auch gleichwertig das SyncLock-Statement benutzt werden). Nach den Änderungen sieht der Beispielcode wie folgt aus:


Public Class ClassA
'erste synchronisierte Methode
Public Sub SyncProc()
Dim i As Int32
SyncLock Me
For i = 0 To 100
Thread.Sleep(20)
Console.Write(".{0}.", i)
Next
End SyncLock
End Sub
'zweite synchronisierte Methode
Public Sub UnsyncProc()
Dim i As Int32
SyncLock Me
For i = 0 To 50
Console.Write("..STOP..")
Thread.Sleep(20)
Next
End SyncLock
End Sub
End Class

Wenn wir die auf diese Weise veränderte Anwendung starten, sehen wir uns bestätigt: Erst werden alle Ausgaben der Zeichenfolge .STOP. erfolgen, daran schließen sich alle Zahlenausgaben an.


Der Monitor kann für ein bestimmtes Objekt nur ein einziges Mal vergeben werden. Greifen mehrere Threads auf verschiedene synchronisierte Methoden desselben Objekts zu, muss der Thread, der als erster die Sperre erhalten hat, seine Operationen vollständig abschließen, bevor der zweite Thread das Recht auf den Monitor erhält.


Daraus kann eine Schlussfolgerung gezogen werden. Wenn Sie zwei Methoden haben, die eine Synchronisation erfordern, dabei jedoch nicht voneinander abhängen, müssen Sie diese Methoden in separaten Klassen implementieren.

Jetzt erklärt sich auch, weshalb den Methoden Enter und Exit der Klasse Monitor eine Objektreferenz übergeben wird: Die Sperre bezieht sich auf das gesamte Objekt.

Die Methoden »Wait« und »Pulse«

Die Klasse Monitor ist nicht instanziierbar, da jedem Objekt nur ein Monitor zugeordnet werden kann. Mehrere Objekte können den Anspruch auf die Nutzung des Monitors eines anderen Objekts erheben, aber nur einem Objekt aus der Warteschlange wird er zugestanden.

Stellen Sie sich den Monitor wie ein Fernglas vor, das Sie mit in den Urlaub genommen haben, um damit die Landschaft aus der optischen Nähe zu betrachten. Solange Sie das Fernglas benutzen, hat keine andere Person die Möglichkeit, die schönen Dingen der Natur aus der Nähe zu betrachten. Eine andere Person, die auch einmal einen Blick durch das Fernglas werfen möchte, kann dieses nicht nehmen, weil es in Gebrauch ist, und wird sich in die Warteschlange auf dieses Objekt einreihen müssen. Sobald Sie das Fernglas zur Seite gelegt haben, kann es von einer Person aus der Warteschlange aufgenommen werden, während die möglicherweise anderen eingereihten Personen sich weiter gedulden müssen.

Nehmen wir jetzt an, Sie wären mit einem Ihrer Freunde im Urlaub. Während Sie durch das Fernglas schauen, meldet Ihr Freund seinen berechtigten Anspruch an. Sie legen das Fernglas freiwillig zur Seite, informieren Ihren Freund darüber, dass er es nun benutzen darf, und treten Ihrerseits selbst zurück in die Warteschlange.

Die letzten beiden Aktionen lassen sich auch auf den Monitor projizieren. Sobald Sie das Fernglas freiwillig zur Seite legen mit der Absicht, es zu einem späteren Zeitpunkt noch einmal aufzugreifen, versetzen Sie sich in den Wartezustand und begeben sich in die Warteschlange. Die Monitorklasse beschreibt diese Operation mit der Methode Wait. Das Informieren des nächsten Interessenten in der Warteschlange kommt der Methode Monitor.Pulse zu. Wait und Pulse können nur innerhalb eines Synchronisationsblocks aufgerufen werden. Mit Wait wird der aktuelle Thread blockiert und gleichzeitig die Sperrung des Objekts aufgehoben. Damit kann ein anderer Thread das freigegebene Objekt nutzen. Schauen wir uns eine Definition der überladenen Wait-Methode an:


Public Shared Function Wait(Object) As Boolean

Der Parameter nimmt die Referenz auf das Objekt entgegen, dessen Sperrung aufgehoben werden soll. Ein wenig sonderbar verhält sich der Rückgabewert. Er ist True, wenn kein anderer Thread das Objekt sperrt und der aktuelle Thread selbst die Verantwortung der Sperrung übernimmt. Ansonsten kommt kein boolescher Wert zurück, was einer Einreihung in die Warteschlange nachkommt. Damit bietet sich Wait auch dazu an, als Bedingung für den Eintritt in eine Schleife behilflich zu sein:


Do While Monitor.Wait(obj)
'Thread tritt in den synchronisierten Block ein
Loop

Es besteht ein großer Unterschied zwischen einem Thread, der mit Enter auf den Eintritt in eine synchronisierte Methode wartet, und einem Thread, der sich mit Wait in den Wartezustand versetzt hat. Ein Thread, der eine synchronisierte Methode mit Monitor.Enter betreten möchte, befindet sich im Zustand »bereit«. Er reiht sich in die Threads ein, die auf Anweisung des Schedulers hin ein Segment der Zeitscheibe erhalten. Ein Thread, der mit Wait die Sperrung eines Objekts aufgehoben hat, befindet sich in einer Warteliste. Allerdings nicht in die Warteliste, aus welcher der Scheduler einem bereiten Thread die CPU zuteilt, sondern eine Warteliste aller derer Threads, die durch den Zustand »warten« (Waiting) beschrieben werden.

Um einen Thread aus seinem Wartezustand zu holen, muss ein anderer Thread die Methode Monitor.Pulse oder Monitor.PulseAll auf demselben Objekt aufrufen. Das Problem ist, dass Pulse keinen bestimmten wartenden Thread aus der Liste holt, sondern – falls sich mehrere Threads darin befinden – einen mehr oder weniger willkürlich gewählten, während mit PulseAll alle Threads den Zustand warten aufgeben und in bereit übergehen. Damit stehen Sie wieder in der Warteschlange der Zeitscheibe – der Scheduler kann ihnen wieder Prozessorzeit zuteilen.

Ein Thread, der mit Wait die Sperrung des kritischen Codebereichs aufgehoben hat, wartet auf einen Anstoß von außen, um wieder aktiv werden zu können, denn er selbst hat keine Möglichkeit mehr, diesen Zustand zu beenden. Wenn kein anderer Thread Pulse oder PulseAll aufruft, wird ein wartender Thread daher niemals mehr laufen können.


Im Extremfall kann der Wartezustand der Threads einer Anwendung zu einem Phänomen führen, das unter der Bezeichnung Deadlock bekannt ist. Dabei befinden sich ausnahmslos alle Threads im blockierten Wartezustand. Die Anwendung hängt sich in diesem Moment auf.


Wir wollen nun die vorgestellten Methoden in einem Beispiel testen. Dazu werden wir eine Anwendung entwickeln, die in Lage ist, Zahlen zu erzeugen. Das ist eigentlich nichts Weltbewegendes und haben wir auch schon in den anderen Anwendungen gemacht. Das Besondere sei jedoch, dass jede generierte Zahl genau einmal von einem Verbraucher ausgewertet werden soll. Dieser Verbrauch soll durch eine Konsolenausgabe simuliert werden. Erzeuger und Verbraucher sollen in je einem eigenen Thread laufen.


' ----------------------------------------------------------
' Beispiel: ...\ Kapitel 11\Zahlenkonsument
' ----------------------------------------------------------
Imports System.Threading
Module Module1
Public finished As Boolean = False
Public thread1Waiting As Boolean = False
Public thread2Waiting As Boolean = False
Sub Main()
Dim zahl As New MyNumber
Dim prod As ProduceNumber = New ProduceNumber(zahl)
Dim cons As ConsumeNumber = New ConsumeNumber(zahl)
Dim thread1, thread2 As Thread
' Threads instanziieren
thread1 = New Thread(New ThreadStart(AddressOf prod.MakeNumber))
thread2 = New Thread(New ThreadStart(AddressOf cons.GetNumber))
' Threads starten
thread1.Start()
thread2.Start()
Console.ReadLine()
End Sub
End Module
' ------ erzeugt eine Zahl ------------
Class ProduceNumber
Private obj As MyNumber
Public Sub New(ByVal obj As MyNumber)
Me.obj = obj
End Sub
Public Sub MakeNumber()
Dim rnd As New Random
Monitor.Enter(obj)
Dim i As Integer
For i = 0 To 10
thread1Waiting = True
' falls Konsumer-Thread noch nicht im Wartezustand,
' selbst in den Wartezustand gehen
If (thread2Waiting = False) Then
Monitor.Wait(obj)
End If
obj.Number = rnd.Next(0, 1000)
Console.WriteLine("Nummer {0} erzeugt", obj.Number)
' dem nächsten in der Warteschlange stehenden Objekt
' den Monitor übergeben
Monitor.Pulse(obj)
thread2Waiting = False
Next
finished = True
Monitor.Exit(obj)
End Sub
End Class
' --------- verbraucht eine Zahl -------------
Class ConsumeNumber
Private obj As MyNumber
Public Sub New(ByVal obj As MyNumber)
Me.obj = ob
End Sub
Public Sub GetNumber()
Monitor.Enter(obj)
' wenn sich der Erzeuger-Thread im Wartezustand
' befindet, ihn 'bereit' schalten
If (thread1Waiting) Then
Monitor.Pulse(obj)
End If
thread2Waiting = True
Do While (Monitor.Wait(obj))
Console.WriteLine("Nummer {0} verbraucht", obj.Number)
Monitor.Pulse(obj)
If (finished) Then
Thread.CurrentThread.Abort()
End If
loop
Monitor.Exit(obj)
End Sub
End Class
' ------------ repräsentiert eine Zahl -------------------
Class MyNumber
Private intValue As Integer
Public Property Number() As Integer
Get
Return intValue
End Get
Set(ByVal value As Integer)
intValue = value
End Set
End Property
End Class

Der Kern der Anwendung wird durch die beiden Klassen ProduceNumber und ConsumeNumber beschrieben. ProduceNumber erzeugt mit der Methode MakeNumber auf Basis des Zufallszahlengenerators Zahlen zwischen 0 und 999 und schreibt diese in das Feld eines Objekts vom Typ der Klasse MyNumber, das in Main erzeugt wird und dessen Referenz den Konstruktoren der Klassen ConsumeNumber und ProduceNumber übergeben wird. Damit ist sichergestellt, dass sowohl Erzeuger als auch Verbraucher mit demselben MyNumber-Objekt operieren.

Betrachten wir die prinzipielle Arbeitsweise des Verbrauchers und des Konsumenten unter der Prämisse, dass zuerst der Erzeuger-Thread und danach der Verbraucher-Thread gestartet wird. Der gesamte Code in der Routine MakeNumber ist synchronisiert. Insgesamt werden elf Zahlen in einer Schleife erzeugt. Direkt nach dem Schleifeneintritt wird die Wait-Methode des Monitors aufgerufen und die Sperre des Objekts vom Typ MyNumber aufgehoben. Jetzt kann ein anderer Thread auf das freigegebene MyNumber-Objekt zugreifen und die Zahl »verbrauchen«, die einen Schleifendurchlauf zuvor erzeugt worden ist. Der Erzeuger-Thread verharrt so lange in Wartestellung, bis der Verbraucher-Thread Pulse aufruft und den Erzeugerthread wieder in den Zustand bereit versetzt. Ist dessen Wartezustand aufgehoben, wird die nächste Zahl erzeugt. Bevor der nächste Schleifendurchlauf ausgeführt wird, wird der Verbraucher mit Pulse in den Zustand bereit erhoben.

In der Methode GetNumber des Verbrauchers ist der Programmcode ebenfalls synchronisiert. Direkt nach dem Eintritt in den Synchronisierungsabschnitt wird Pulse aufgerufen, um den wartenden Erzeuger-Thread nach dem Start der Anwendung in den Zustand bereit zu versetzen. Anschließend ruft der Konsument Wait auf. Damit wird der Monitor des Objekts an den Erzeuger weitergegeben, der eine Zahl erzeugt. Gibt der Erzeuger die Sperre an den Konsumenten zurück, wird die neue Zahl zuerst an der Konsole angezeigt und anschließend Pulse aufgerufen. Jetzt ist der Erzeuger-Thtread wieder im Zustand bereit, während der Konsument seinerseits anschließend den Monitor freigibt und sich in den Wartezustand versetzt.

Damit der Verbraucher-Thread überhaupt erfährt, wann der Erzeuger die letzte Zahl bereitgestellt hat, ist die boolesche Variable finished deklariert, die vom Erzeuger nach dem letzten Schleifendurchlauf auf True gesetzt wird.

Soweit zur prinzipiellen Arbeitsweise. Es gibt aber noch ein Problem, dem wir bisher noch keine Beachtung geschenkt haben: Wir können nämlich nicht garantieren, dass der Erzeuger-Thread als Erstes gestartet wird. Erhält nach dem Starten der Anwendung der Konsument vor dem Erzeuger die CPU, würde es zu einem klassischen Deadlock kommen, wenn sich beide Threads gleichzeitig im Zustand warten befinden. Wir müssen also eine genaue Steuerung des Programmablaufs in der Weise erzwingen, dass sich der Verbraucher-Thread im Wartezustand befindet, wenn die erste Zahl erzeugt wird. Diese Steuerung wird über die beiden booleschen Variablen thread1Waiting und thread2Waiting erreicht, deren Auswertung garantiert, dass sich – unabhängig von der Startreihenfolge – zu einem gegebenen Zeitpunkt immer nur ein Thread im Wartezustand befindet.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 11.9     Die Ausgabe des Projekts »Zahlenkonsument«


Galileo Computing

11.3.3 Das Synchronisationsobjekt »Mutex«  downtop

Die Klasse Monitor eignet sich nur zur Synchronisation von Threads, die innerhalb eines Prozessraums laufen. Manchmal müssen Abläufe aber auch über Prozessgrenzen hinweg synchronisiert werden. In diesen Fällen müssen Sie die Klasse Mutex einsetzen. Ich möchte Ihnen hierzu ein Beispiel zeigen, bei dem ein Mutex-Objekt dazu benutzt wird zu verhindern, dass eine Anwendung mehrfach gestartet werden kann.


' ----------------------------------------------------------
' Beispiel: ...\Kapitel 11\MutexDemo
' ----------------------------------------------------------
Imports System.Threading
Imports System.Windows.Forms
Module Module1
Private myMutex As Mutex
Sub Main()
If (IsApplicationStarted()) Then
Console.WriteLine("Die Anwendung wurde bereits gestartet")
Console.WriteLine("Ein zweiter Start ist nicht möglich.")
Else
Console.WriteLine("Die Anwendung wird gestartet.")
Console.WriteLine("Die Anwendung läuft.")
End If
Console.ReadLine()
End Sub
Public Function IsApplicationStarted() As Boolean
Dim mutexName As String = Application.ProductName
myMutex = New Mutex(False, mutexName)
If (myMutex.WaitOne(0, True)) Then
Return False
Else
Return True
End If
End Function
End Module

Ein Mutex ist ein einfaches Systemobjekt und durch einen eindeutigen Namen gekennzeichnet. Ein Mutex-Objekt gestattet nur jeweils einem Thread exklusiven Zugriff auf die gemeinsam genutzte Ressource. In unserem Beispiel wird das die Anwendung selbst sein. Wenn ein Thread ein Mutex-Objekt erhält, wird ein zweiter Thread, der dieses Objekt abruft, so lange angehalten, bis der erste Thread das Mutex-Objekt freigibt.

Die Klasse Mutex stellt mehrere Konstruktoren zur Verfügung. Für unsere Belange ist der geeignet, dem wir den Namen des Mutex mitteilen können. Die Schwierigkeit besteht bei der Namensvergabe darin, dass er systemeindeutig sein muss, um den Mutex identifizieren zu können. Es bietet sich hier der Anwendungsname an, obwohl dieser auch keine Garantie gibt. Möglicherweise müssen Sie hier noch Zusatzinformationen hinzufügen, um die Eindeutigkeit zumindest mit hoher Wahrscheinlichkeit zu gewährleisten. Der entsprechende Konstruktor erwartet darüber hinaus auch noch einen booleschen Wert, der angibt, ob dem aufrufenden Thread der anfängliche Besitz des Mutex zugewiesen werden soll. Er ist True, um dem aufrufenden Thread den anfänglichen Besitz des benannten Mutex zuzuweisen.

Über die Methode WaitOne kann eine Ressource das Mutex-Objekt anfordern. Ist der Rückgabewert True, ist das Objekt nicht im Besitz eines anderen Threads, ansonsten wäre der Rückgabewert False.

WaitOne werden zwei Argumente übergeben: Das erste beschreibt ein Zeitintervall, das angibt, wie lange auf ein Signal gewartet werden soll. Über das zweite Argument können Sie bei Anwendungen, die vor dem Warten auf den Mutex den Zugriff auf Objekte oder Klassen sperren, festlegen, dass die Sperrung vor dem Warten aufgehoben und nach dem Warten wieder gesetzt wird. Für uns hat dieser Parameter keine Bedeutung, wir setzen ihn auf True.

Im Beispielprogramm dient die Methode IsApplicationStarted dazu zu prüfen, ob das Mutex-Objekt sich bereits im Besitz eines anderen Threads befindet. Je nachdem, wie das Ergebnis der Prüfung ausfällt, wird eine entsprechende Konsolenausgabe erscheinen.


Galileo Computing

11.3.4 Das Attribut »MethodImpl«  toptop

Es gibt noch eine weitere Alternative, Synchronisierung zwischen mehreren Threads zu erzielen: mittels des Attributs MethodImpl. Die zugrunde liegende Klasse ist im Namespace System.Runtime.CompilerServices zu finden. Das Attribut kann nur auf Konstruktoren und Methoden angewendet werden. Es ersetzt die Klasse Monitor mit dem Unterschied, dass nicht nur ein bestimmtes Codesegment betroffen ist, sondern in einem Zug gleich die gesamte Methode. Somit kann auch nur immer ein Thread gleichzeitig diese Methode ausführen. Dazu ein Beispiel:


<MethodImpl(MethodImplOptions.Synchronized)> _
Public Sub Calculate()
...
End Sub

Dem Attribut MethodImpl können verschiedene Parameter übergeben werden. Zur Synchronisation verwenden Sie MethodImplOptions.Synchronized.

 <<   zurück
  
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Visual Basic 2012






 Visual Basic 2012


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


Zum Katalog: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Katalog: Windows Presentation Foundation






 Windows Presentation
 Foundation


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Rheinwerk Verlag GmbH 2007
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Rheinwerk Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de