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

Inhaltsverzeichnis
1 Einführung
2 Grundlagen der Sprachsyntax
3 Klassendesign
4 Weitere Datentypen
5 Multithreading
6 Collections und LINQ
7 Eingabe und Ausgabe
8 Anwendungen: Struktur und Installation
9 Code erstellen und debuggen
10 Einige Basisklassen
11 Windows-Anwendungen erstellen
12 Die wichtigsten Steuerelemente
13 Tastatur- und Mausereignisse
14 MDI-Anwendungen
15 Grafiken mit GDI+
16 Drucken
17 Entwickeln von Steuerelementen
18 Programmiertechniken
19 WPF – Grundlagen
20 Layoutcontainer
21 WPF-Steuerelemente
22 Konzepte von WPF
23 Datenbankverbindung mit ADO.NET
24 Datenbankabfragen mit ADO.NET
25 DataAdapter
26 Offline mit DataSet
27 Datenbanken aktualisieren
28 Stark typisierte DataSets
A Anhang: Einige Übersichten
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Visual Basic 2008 von Andreas Kuehnel, Stephan Leibbrandt
Das umfassende Handbuch
Buch: Visual Basic 2008

Visual Basic 2008
3., aktualisierte und erweiterte Auflage, geb., mit DVD
1.323 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1171-0
Pfeil 5 Multithreading
Pfeil 5.1 Start eines Threads
Pfeil 5.1.1 Parameterloser Start
Pfeil 5.1.2 Start mit Parameter
Pfeil 5.1.3 Hintergrundthreads
Pfeil 5.1.4 Threadeigenschaften
Pfeil 5.2 Zusammenspiel
Pfeil 5.2.1 Priorität
Pfeil 5.2.2 Warten mit Sleep
Pfeil 5.2.3 Unterbrechen mit Interrupt
Pfeil 5.2.4 Abbruch mit Abort
Pfeil 5.2.5 Warten mit Join
Pfeil 5.2.6 Warten erzwingen mit Suspend
Pfeil 5.2.7 Threadzustände
Pfeil 5.2.8 Die Klasse Thread
Pfeil 5.3 Gesicherter Datenaustausch
Pfeil 5.3.1 Objekte sperren mit Monitor
Pfeil 5.3.2 Codebereiche mit SyncLock sperren
Pfeil 5.3.3 Sperrung mit Zustand
Pfeil 5.3.4 Atomare Operationen
Pfeil 5.3.5 Attributgesteuerte Synchronisation
Pfeil 5.4 Asynchrone Methodenaufrufe
Pfeil 5.4.1 Aufruf
Pfeil 5.4.2 Rückruf
Pfeil 5.4.3 Zustand
Pfeil 5.4.4 Rückgabe
Pfeil 5.4.5 ByRef-Parameter
Pfeil 5.5 Threadpools


Rheinwerk Computing - Zum Seitenanfang

5.3 Gesicherter Datenaustausch Zur nächsten ÜberschriftZur vorigen Überschrift

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) 
' Anweisungen 
Freigabe(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.


Rheinwerk Computing - Zum Seitenanfang

5.3.1 Objekte sperren mit Monitor Zur nächsten ÜberschriftZur vorigen Überschrift

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) 
Sub PulseAll(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 
Function Wait(obj As Object[, timeout As TimeSpan]) 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.


Tabelle 5.4 Methoden der Klasse »System.Threading.Monitor« (kursiv = optional)

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



Rheinwerk Computing - Zum Seitenanfang

5.3.2 Codebereiche mit SyncLock sperren Zur nächsten ÜberschriftZur vorigen Überschrift

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 
  'Anweisungen 
End SyncLock

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.

Rheinwerk Computing - Zum Seitenanfang

5.3.3 Sperrung mit Zustand Zur nächsten ÜberschriftZur vorigen Überschrift

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).


Tabelle 5.5 Mitglieder der Klasse »System.Threading.WaitHandle« (S = Shared, C = Const, P = Property)

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.


Tabelle 5.6 Methoden der Klasse »System.Threading.EventWaitHandle« (S = Shared, kursiv = optional)

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 
  mutex.WaitOne() 
  'Anweisungen 
Finally 
  mutex.ReleaseMutex() 
End 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.


Tabelle 5.7 Methoden der Klasse »System.Threading.Mutex« (S = Shared, kursiv = optional)

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.


Tabelle 5.8 Methoden der Klasse »System.Threading.Semaphore« (S = Shared, kursiv = optional)

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



Rheinwerk Computing - Zum Seitenanfang

5.3.4 Atomare Operationen Zur nächsten ÜberschriftZur vorigen Überschrift

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).


Tabelle 5.9 Methoden der Klasse »System.Threading.Interlock« (alle Shared, kursiv = ByRef)

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



Rheinwerk Computing - Zum Seitenanfang

5.3.5 Attributgesteuerte Synchronisation topZur vorigen Überschrift

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.


Tabelle 5.10 Konstanten der Enumeration »System.Runtime.CompilerServices.MethodImplOptions«

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 /


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Visual Basic 2008
Visual Basic 2008
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
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 2009
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]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de