Skip to content

8. Ausführungsthreads

8.1. Einführung

Wenn eine Anwendung gestartet wird, wird sie in einem Ausführungsstrom ausgeführt, der als Thread bezeichnet wird. Die Klasse .NET, die ein thread modelliert, ist die Klasse System.Threading.Thread und hat folgende Definition:

Image

Wir werden nur einige der Eigenschaften und Methoden dieser Klasse verwenden:

CurrentThread - propriété statique
gibt den aktuell ausgeführten Thread zurück
Name - propriété d'objet
Name des Threads
isAlive - propriété d'objet
gibt an, ob der Thread aktiv ist (true) oder nicht (false)
Start - méthode d'objet
Startet die Ausführung eines Threads
Abort - méthode d'objet
beendet die Ausführung eines Threads endgültig
Sleep(n) - méthode statique
unterbricht die Ausführung eines Threads für n Millisekunden
Suspend() - méthode d'objet
unterbricht vorübergehend die Ausführung eines Threads
Resume() - méthode d'objet
setzt die Ausführung eines angehaltenen Threads fort
Join() - méthode d'objet
blockierende Operation – wartet auf das Ende des Threads, um zur nächsten Anweisung überzugehen

Sehen wir uns eine erste Anwendung an, die das Vorhandensein eines Hauptausführungsthreads verdeutlicht, in dem die Funktion Main einer Klasse ausgeführt wird:


' Verwendung von Threads
Imports System
Imports System.Threading

Public Module thread1
    Public Sub Main()
        ' aktuellen Thread initialisieren
        Dim main As Thread = Thread.CurrentThread
        ' Anzeige
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' Name wird geändert
        main.Name = "main"
        ' Überprüfung
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' Endlosschleife
        While True
            ' Anzeige
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' vorübergehende Unterbrechung
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Die Bildschirmausgabe:

dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59

Das vorstehende Beispiel veranschaulicht folgende Punkte:

  • Die Funktion Main wird ordnungsgemäß in einem Thread ausgeführt
  • man hat über Thread.CurrentThread Zugriff auf die Eigenschaften dieses Threads
  • die Rolle der Methode Sleep. Hier versetzt sich der Thread, der Main ausführt, zwischen zwei Anzeigen regelmäßig für 1 Sekunde in den Ruhezustand.

8.2. Erstellung von Ausführungsthreads

Es gibt Anwendungen, in denen Codeabschnitte „gleichzeitig“ in verschiedenen Ausführungsthreads ausgeführt werden. Wenn man sagt, dass threads gleichzeitig ausgeführt werden, handelt es sich oft um eine sprachliche Ungenauigkeit. Verfügt der Rechner nur über einen Prozessor – was nach wie vor häufig der Fall ist –, teilen sich die threads diesen Prozessor: Sie haben abwechselnd für einen kurzen Moment (einige Millisekunden) Zugriff darauf. Dies erzeugt die Illusion einer parallelen Ausführung. Die einem thread zugewiesene Zeit hängt von verschiedenen Faktoren ab, darunter seiner Priorität, die einen Standardwert hat, aber auch programmgesteuert festgelegt werden kann. Wenn ein thread über den Prozessor verfügt, nutzt er diesen normalerweise während der gesamten ihm zugewiesenen Zeit. Er kann ihn jedoch vorzeitig freigeben:

  • indem es auf ein Ereignis wartet (wait, join, suspend)
  • indem er für eine bestimmte Zeit in den Ruhezustand versetzt wird (sleep)
  1. Ein Thread T wird zunächst durch seinen Konstruktor erstellt
Public Sub New(ByVal start As ThreadStart)

ThreadStart ist vom Typ „Delegate“ und definiert den Prototyp einer Funktion ohne Parameter:

Public Delegate Sub ThreadStart()

Eine klassische Konstruktion sieht wie folgt aus:

dim T as Thread=new Thread(new ThreadStart(run));

Die als Parameter übergebene Funktion run wird beim Start des Threads ausgeführt.

  1. Die Ausführung des Threads T wird durch T.Start() gestartet: Die Funktion [run], die an den Konstruktor von T übergeben wird, wird dann vom Thread T ausgeführt. Das Programm, das die Anweisung T.start() ausführt, wartet nicht auf das Ende der Aufgabe T: Es fährt sofort mit der folgenden Anweisung fort. Es laufen also zwei Aufgaben parallel ab. Oft müssen sie miteinander kommunizieren können, um zu erfahren, wie weit die gemeinsam auszuführende Arbeit fortgeschritten ist. Dies ist das Problem der Thread-Synchronisation.
  1. Einmal gestartet, läuft der Thread autonom ab. Er wird beendet, sobald die von ihm ausgeführte Funktion start ihre Arbeit beendet hat.
  1. Man kann bestimmte Signale an die Aufgabe T senden:
    1. T.Suspend() weist sie an, vorübergehend anzuhalten
    2. T.Resume() weist sie an, ihre Arbeit fortzusetzen
    3. T.Abort() weist sie an, endgültig anzuhalten
  1. Man kann auch mit T.join() auf das Ende ihrer Ausführung warten. Hierbei handelt es sich um eine blockierende Anweisung: Das Programm, das sie ausführt, wird blockiert, bis die Aufgabe T ihre Arbeit beendet hat. Dies ist eine Möglichkeit der Synchronisation.

Betrachten wir das folgende Programm:


' Optionen
Option Strict On
Option Explicit On 

' Namensräume
Imports System
Imports System.Threading

Module thread2
    Public Sub Main()
        ' aktuellen Thread initialisieren
        Dim main As Thread = Thread.CurrentThread
        ' Thread mit einem Namen versehen
        main.Name = "main"

        ' Erstellung von Ausführungsthreads
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' Erstellung des Threads i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' Der Name des Threads wird festgelegt
            tâches(i).Name = "tache_" & i
            ' Start der Ausführung des Threads i
            tâches(i).Start()
        Next i
        ' Ende der Hauptschleife
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub

    Public Sub affiche()
        ' Anzeige des Ausführungsbeginns
        Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
        ' 1 Sekunde lang in den Ruhezustand versetzen
        Thread.Sleep(1000)
        ' Anzeige des Endes der Ausführung
        Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
    End Sub
End Module

Der Hauptthread, der die Funktion Main ausführt, erstellt fünf weitere Threads, die die statische Methode affiche ausführen sollen. Die Ergebnisse sind wie folgt:

dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54

Diese Ergebnisse sind sehr aufschlussreich:

  • Zunächst einmal lässt sich erkennen, dass der Start eines Threads nicht blockierend ist. Die Methode Main hat die Ausführung von 5 Threads parallel gestartet und ihre eigene Ausführung vor diesen abgeschlossen. Die Operation
            ' Start der Ausführung des Threads i
            tâches(i).Start()

löst die Ausführung des Threads „tâches[i]“ aus, doch danach wird die Ausführung sofort mit der folgenden Anweisung fortgesetzt, ohne auf das Ende der Ausführung des Threads zu warten.

  • Alle erstellten Threads müssen die Methode affiche ausführen. Die Ausführungsreihenfolge ist unvorhersehbar. Auch wenn im Beispiel die Ausführungsreihenfolge der Reihenfolge der Ausführungsanforderungen zu folgen scheint, lassen sich daraus keine allgemeinen Schlussfolgerungen ziehen. Das Betriebssystem verfügt hier über 6 Threads und einen Prozessor. Es verteilt die Prozessorressourcen nach eigenen Regeln auf diese 6 Threads.
  • In den Ergebnissen ist eine Auswirkung der Methode Sleep zu erkennen. Im Beispiel ist es Thread 0, der als erster die Methode affiche ausführt. Die Meldung zum Start der Ausführung wird angezeigt, anschließend führt er die Methode Sleep aus, die ihn für 1 Sekunde anhält. Er verliert daraufhin den Prozessor, der somit für einen anderen Thread verfügbar wird. Das Beispiel zeigt, dass Thread 1 diesen erhält. Thread 1 durchläuft denselben Ablauf wie die anderen Threads. Wenn die einsekündige Wartezeit von Thread 0 abgelaufen ist, kann dessen Ausführung fortgesetzt werden. Das System weist ihm den Prozessor zu, und er kann die Ausführung der Methode affiche abschließen.

Ändern wir unser Programm so, dass die Methode Main mit den folgenden Anweisungen beendet wird:

        ' Ende der Hauptschleife
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Die Ausführung des neuen Programms ergibt:

fin du thread main

Die von der Funktion Main erstellten Threads werden nicht ausgeführt. Dies ist die Anweisung

        Environment.Exit(0)

, die dafür verantwortlich ist: Sie beendet alle Threads der Anwendung und nicht nur den Thread Main. Die Lösung für dieses Problem besteht darin, dass die Methode Main das Ende der Ausführung der von ihr erstellten Threads abwartet, bevor sie selbst beendet wird. Dies kann mit der Methode Join der Klasse Thread erreicht werden:


        ' Warten auf das Ende der Ausführung aller Threads
        For i = 0 To tâches.Length - 1
            ' Warten auf das Ende der Ausführung von Thread i
            tâches(i).Join()
        Next i        'for
        ' Ende der Hauptschleife
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Daraus ergeben sich folgende Ergebnisse:

Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main

8.3. Bedeutung von Threads

Nachdem wir nun die Existenz eines Standard-Threads aufgezeigt haben – nämlich den, der die Methode Main ausführt – und wissen, wie man weitere Threads erstellt, wollen wir uns nun mit dem Nutzen von Threads für uns befassen und erläutern, warum wir sie hier vorstellen. Es gibt eine Art von Anwendungen, die sich gut für den Einsatz von Threads eignen: Client-Server-Anwendungen im Internet. In einer solchen Anwendung beantwortet ein Server auf einem Rechner mit der Bezeichnung S1 Anfragen von Clients auf entfernten Rechnern mit den Bezeichnungen C1, C2, …, Cn.

Wir nutzen täglich Internetanwendungen, die diesem Schema entsprechen: Webdienste, E-Mail, Forenbesuche, Dateiübertragungen … Im obigen Schema muss der Server S1 die Clients Ci gleichzeitig bedienen. Nehmen wir das Beispiel eines Servers FTP (File Transfer Protocol), der Dateien an seine Clients überträgt. Wir wissen, dass eine Dateiübertragung manchmal mehrere Stunden dauern kann. Es kommt natürlich nicht in Frage, dass ein einzelner Client den Server über einen so langen Zeitraum allein beansprucht. Üblicherweise erstellt der Server so viele Ausführungs-Threads, wie es Clients gibt. Jeder Thread ist dann dafür zuständig, sich um einen bestimmten Client zu kümmern. Da der Prozessor zyklisch zwischen allen aktiven Threads des Rechners aufgeteilt wird, verbringt der Server ein wenig Zeit mit jedem Client und gewährleistet so die Parallelität des Dienstes.

8.4. Zugriff auf gemeinsam genutzte Ressourcen

Im oben genannten Client-Server-Beispiel bedient jeder Thread einen Client weitgehend unabhängig. Dennoch kann es vorkommen, dass die Threads zusammenarbeiten müssen, um den von ihrem Client angeforderten Dienst zu erbringen, insbesondere beim Zugriff auf gemeinsam genutzte Ressourcen. Das obige Schema erinnert an die Schalter einer großen Behörde, beispielsweise einer Poststelle, an der an jedem Schalter ein Mitarbeiter einen Kunden bedient. Nehmen wir an, dass diese Mitarbeiter von Zeit zu Zeit Kopien von Dokumenten anfertigen müssen, die ihre Kunden mitgebracht haben, und dass es nur einen Kopierer gibt. Zwei Mitarbeiter können den Kopierer nicht gleichzeitig benutzen. Wenn Mitarbeiter i feststellt, dass der Kopierer von Mitarbeiter j benutzt wird, muss er warten. Diese Situation wird als Zugriff auf eine gemeinsam genutzte Ressource bezeichnet und ist in der Informatik recht schwierig zu handhaben. Nehmen wir folgendes Beispiel:

  • Eine Anwendung erzeugt n Threads, wobei n als Parameter übergeben wird
  • Die gemeinsam genutzte Ressource ist ein Zähler, der von jedem erzeugten Thread erhöht werden muss
  • Am Ende der Anwendung wird der Wert des Zählers angezeigt. Man sollte also n erhalten.

Das Programm lautet wie folgt:


' Optionen
Option Explicit On 
Option Strict On

' Verwendung von Threads
Imports System
Imports System.Threading

Public Class thread3
    ' Klassenvariablen
    Private Shared cptrThreads As Integer = 0

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' Bedienungsanleitung
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' Überprüfung der Anzahl der Argumente
        If args.Length <> 1 Then
            ' Fehler
            Console.Error.WriteLine(syntaxe)
            ' Beenden
            Environment.Exit(1)
        End If
        ' Prüfung der Argumentqualität
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' Fehler
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' Ende
            Environment.Exit(2)
        End Try
        ' Erstellung und Generierung von Threads
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' Erstellung
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' Benennung
            threads(i).Name = "tache_" & i
            ' Start
            threads(i).Start()
        Next i
        ' Warten auf das Ende der Threads
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' affichage compteur
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' erhöht den Thread-Zähler
        ' Zähler auslesen
        Dim valeur As Integer = cptrThreads
        ' Überwachung
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' Warten
        Thread.Sleep(1000)
        ' Zähler erhöhen
        cptrThreads = valeur + 1
        ' Überwachung
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
    End Sub
End Class

Wir werden nicht näher auf den bereits behandelten Teil zur Thread-Erzeugung eingehen. Konzentrieren wir uns stattdessen auf die Methode incrémente, die von jedem Thread verwendet wird, um den statischen Zähler cptrThreads zu inkrementieren.

  1. Der Zähler wird gelesen
  2. Der Thread hält 1 s lang an. Er verliert somit die Prozessorsteuerung
  3. Der Zähler wird erhöht

Schritt 2 dient lediglich dazu, den Thread zu zwingen, die Prozessorsteuerung abzugeben. Diese wird dann an einen anderen Thread übergeben. In der Praxis gibt es keine Garantie dafür, dass ein Thread nicht zwischen dem Zeitpunkt, an dem er den Zähler ausliest, und dem Zeitpunkt, an dem er ihn inkrementiert, unterbrochen wird. Es besteht das Risiko, die Prozessorsteuerung zwischen dem Auslesen des Zählerwerts und dem Schreiben des um 1 erhöhten Werts zu verlieren. Tatsächlich besteht der Inkrementierungsvorgang aus mehreren elementaren Befehlen auf Prozessorebene, die unterbrochen werden können. Schritt 2, die einsekündige Pause, dient daher lediglich dazu, dieses Risiko systematisch zu berücksichtigen. Die erzielten Ergebnisse lauten wie folgt:

dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1

Beim Betrachten dieser Ergebnisse wird deutlich, was hier vor sich geht:

  • Ein erster Thread liest den Zähler aus. Er findet den Wert 0.
  • Er hält 1 s lang an und gibt somit den Prozessor ab
  • Ein zweiter Thread übernimmt dann den Prozessor und liest ebenfalls den Wert des Zählers. Dieser steht immer noch auf 0, da der vorherige Thread ihn noch nicht erhöht hat. Auch er hält 1 Sekunde lang an.
  • Innerhalb von 1 s haben alle 5 Threads Zeit, abzulaufen und den Wert 0 zu lesen.
  • Wenn sie nacheinander wieder aktiv werden, erhöhen sie den von ihnen gelesenen Wert 0 und schreiben den Wert 1 in den Zähler, was das Hauptprogramm (Main) bestätigt.

Wo liegt das Problem? Der zweite Thread hat einen falschen Wert gelesen, da der erste Thread unterbrochen wurde, bevor er seine Aufgabe – die Aktualisierung des Zählers im Fenster – abgeschlossen hatte. Dies führt uns zum Begriff der kritischen Ressource und der kritischen Section eines Programms:

  • Eine kritische Ressource ist eine Ressource, die jeweils nur von einem Thread gleichzeitig gehalten werden kann. In diesem Fall ist der Zähler die kritische Ressource.
  • Ein kritischer Abschnitt eines Programms ist eine Folge von Anweisungen im Ausführungsfluss eines Threads, während der dieser auf eine kritische Ressource zugreift. Es muss sichergestellt werden, dass während dieses kritischen Abschnitts nur dieser Thread Zugriff auf die Ressource hat.

8.5. Exklusiver Zugriff auf eine gemeinsam genutzte Ressource

In unserem Beispiel ist der kritische Abschnitt der Code zwischen dem Auslesen des Zählers und dem Schreiben seines neuen Werts:


        ' Zählerablesung
        Dim valeur As Integer = cptrThreads
        ' Warten
        Thread.Sleep(1000)
        ' Zählerinkrementierung
        cptrThreads = valeur + 1

Um diesen Code auszuführen, muss sichergestellt sein, dass ein Thread allein läuft. Er kann zwar unterbrochen werden, doch während dieser Unterbrechung darf kein anderer Thread denselben Code ausführen. Die Plattform .NET bietet mehrere Werkzeuge, um den exklusiven Zugriff auf kritische Codeabschnitte zu gewährleisten. Wir verwenden die Klasse „Mutex“:

Image

Wir werden hier nur die folgenden Konstruktoren und Methoden verwenden:

public Mutex()
erstellt ein Mutex-Objekt
public bool WaitOne()
Der Thread T1, der die Operation M.WaitOne() ausführt, fordert die Eigentumsrechte am Synchronisationsobjekt M an. Wenn der Mutex M von keinem Thread gehalten wird (was zu Beginn der Fall ist), wird er dem Thread T1 „übergeben“, der ihn angefordert hat. Wenn wenig später ein Thread T2 dieselbe Operation ausführt, wird er blockiert. Ein Mutex kann nämlich nur einem Thread gehören. Er wird freigegeben, sobald der Thread T1 den von ihm gehaltenen Mutex M freigibt. So können mehrere Threads in Erwartung des Mutex M blockiert sein.
public void
ReleaseMutex()
Der Thread T1, der die Operation M.ReleaseMutex() ausführt, gibt den Besitz des Mutex M.Lorsque auf; der Thread T1 verliert die Prozessorsteuerung, das System kann ihn an einen der auf den Mutex M wartenden Threads vergeben. Nur einer erhält ihn nacheinander, die anderen, die auf M warten, bleiben blockiert

Ein Mutex M verwaltet den Zugriff auf eine gemeinsam genutzte Ressource R. Ein Thread fordert die Ressource R über M.WaitOne() an und gibt sie über M.ReleaseMutex() zurück. Ein kritischer Codeabschnitt, der jeweils nur von einem einzigen Thread ausgeführt werden darf, ist eine gemeinsam genutzte Ressource. Die Synchronisation der Ausführung des kritischen Abschnitts kann wie folgt erfolgen:

M.WaitOne()
' Nur dieser Thread darf hier eintreten
' kritischer Abschnitt
....
M.ReleaseMutex()

wobei M ein Objekt vom Typ Mutex ist. Man darf natürlich niemals vergessen, ein nicht mehr benötigtes Mutex freizugeben, damit ein anderer Thread in den kritischen Abschnitt eintreten kann; andernfalls erhalten die Threads, die auf einen nie freigegebenen Mutex warten, niemals Zugriff auf den Prozessor. Außerdem muss eine Deadlock-Situation (deadlock) vermieden werden, in der zwei Threads gegenseitig aufeinander warten. Betrachten wir die folgenden, zeitlich aufeinanderfolgenden Aktionen:

  • Ein Thread T1 erlangt die Kontrolle über einen Mutex M1, um Zugriff auf eine gemeinsam genutzte Ressource R1 zu erhalten
  • Ein Thread T2 erlangt die Kontrolle über einen Mutex M2, um auf eine gemeinsam genutzte Ressource R2 zugreifen zu können
  • Der Thread T1 fordert den Mutex M2 an. Er wird blockiert.
  • Der Thread T2 fordert den Mutex M1 an. Er wird blockiert.

Hier warten die Threads T1 und T2 gegenseitig aufeinander. Dieser Fall tritt auf, wenn Threads zwei gemeinsam genutzte Ressourcen benötigen: die Ressource R1, die durch den Mutex M1 gesteuert wird, und die Ressource R2, die durch den Mutex M2 gesteuert wird. Eine mögliche Lösung besteht darin, beide Ressourcen gleichzeitig mithilfe eines einzigen Mutex M anzufordern. Dies ist jedoch nicht immer möglich, wenn dies beispielsweise zu einer langen Blockierung einer ressourcenintensiven Ressource führt. Eine andere Lösung besteht darin, dass ein Thread, der über M1 verfügt und M2 nicht erhalten kann, daraufhin M1 freigibt, um eine gegenseitige Blockade zu vermeiden. Wenn wir das soeben Gesehene auf das vorherige Beispiel anwenden, sieht unsere Anwendung wie folgt aus:


' Optionen
Option Explicit On 
Option Strict On

' Verwendung von Threads
Imports System
Imports System.Threading

Public Class thread4
    ' Klassenvariablen
    Private Shared cptrThreads As Integer = 0    ' compteur de threads
    Private Shared autorisation As Mutex

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' Bedienungsanleitung
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' Überprüfung der Anzahl der Argumente
        If args.Length <> 1 Then
            ' Fehler
            Console.Error.WriteLine(syntaxe)
            ' Beenden
            Environment.Exit(1)
        End If
        ' Prüfung der Argumentqualität
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
        End Try

        ' Initialisierung der Zugriffsberechtigung für einen kritischen Abschnitt
        autorisation = New Mutex

        ' Erstellung und Generierung von Threads
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' Erstellung
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' Benennung
            threads(i).Name = "tache_" & i
            ' Start
            threads(i).Start()
        Next i
        ' Warten auf das Ende der Threads
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' Anzeige des Zählers
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' erhöht den Thread-Zähler
        ' Berechtigung zum Betreten des kritischen Abschnitts wird angefordert
        autorisation.WaitOne()
        ' Zähler auslesen
        Dim valeur As Integer = cptrThreads
        ' Nachverfolgung
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' Warten
        Thread.Sleep(1000)
        ' Zähler inkrementieren
        cptrThreads = valeur + 1
        ' Nachverfolgung
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' Zugriffsberechtigung wird erteilt
        autorisation.ReleaseMutex()
    End Sub
End Class

Die erzielten Ergebnisse entsprechen den Erwartungen:

dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5

8.6. Ereignisgesteuerte Synchronisation

Betrachten wir die folgende Situation, die manchmal als Produzenten-Konsumenten-Situation bezeichnet wird.

  1. Es gibt ein Array, in das einige Prozesse Daten schreiben (die Produzenten) und andere diese lesen (die Konsumenten).
  2. Die Produzenten sind untereinander gleich, schließen sich jedoch gegenseitig aus: Es kann jeweils nur ein Produzent seine Daten in das Array schreiben.
  3. Die Konsumenten sind untereinander gleich, schließen sich jedoch gegenseitig aus: Es kann jeweils nur ein Konsument die im Array abgelegten Daten lesen.
  4. Ein Verbraucher kann die Daten aus dem Array nur lesen, wenn ein Produzent Daten dort abgelegt hat, und ein Produzent kann nur dann neue Daten in das Array ablegen, wenn die darin enthaltenen Daten bereits verbraucht wurden.

In dieser Darstellung lassen sich zwei gemeinsam genutzte Ressourcen unterscheiden:

    1. die beschreibbare Tabelle
    2. das Lese-Array

Der Zugriff auf diese beiden gemeinsam genutzten Ressourcen kann, wie zuvor beschrieben, durch Mutexe gesteuert werden, wobei für jede Ressource ein Mutex verwendet wird. Sobald ein Verbraucher die Tabelle im Lesemodus erhalten hat, muss er überprüfen, ob sich tatsächlich Daten darin befinden. Dazu wird ein Ereignis verwendet, um ihn darüber zu informieren. Ebenso muss ein Produzent, der die Tabelle im Schreibmodus erhalten hat, warten, bis ein Verbraucher sie geleert hat. Auch hier wird ein Ereignis verwendet.

Die verwendeten Ereignisse sind Teil der Klasse AutoResetEvent:

Image

Dieser Ereignistyp entspricht einem booleschen Wert, vermeidet jedoch aktive oder semi-aktive Wartezeiten. Wenn also die Schreibberechtigung durch einen booleschen Wert peutEcrire gesteuert wird, führt ein Produzent vor dem Schreiben einen Code der folgenden Art aus:

while(peutEcrire==false)        ' attente active

oder

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

Bei der ersten Methode beansprucht der Thread den Prozessor unnötig. Bei der zweiten überprüft er alle 100 ms den Status des booleschen Werts peutEcrire. Mit der Klasse AutoResetEvent lässt sich das noch verbessern: Der Thread fordert an, geweckt zu werden, sobald das Ereignis, auf das er wartet, eingetreten ist:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

Die Operation

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

setzt den booleschen Wert peutEcrire auf false. Die Operation

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

wird von einem Thread ausgeführt und bewirkt, dass dieser weiterläuft, wenn der boolesche Wert peutEcrire wahr ist; andernfalls wird er blockiert, bis der Wert wahr wird. Ein anderer Thread setzt ihn durch die Operation peutEcrire.Set() auf „wahr“ oder durch die Operation peutEcrire.Reset() auf „falsch“.

Das Produzenten-Verbraucher-Programm lautet wie folgt:


' Verwendung von Lese- und Schreib-Threads
' veranschaulicht die gleichzeitige Nutzung gemeinsam genutzter Ressourcen und Synchronisation

' Optionen
Option Explicit On 
Option Strict On

' Verwendung von Threads
Imports System
Imports System.Threading

Public Class lececr

    ' Klassenvariablen
    Private Shared data(5) As Integer    ' ressource partagée entre threads lecteur et threads écrivain
    Private Shared lecteur As Mutex    ' variable de synchronisation pour lire le tableau
    Private Shared écrivain As Mutex    ' variable de synchronisation pour écrire dans le tableau
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' un générateur de nombres aléatoires
    Private Shared peutLire As AutoResetEvent    ' signale qu'on peut lire le contenu de data
    Private Shared peutEcrire As AutoResetEvent

    Public Shared Sub Main(ByVal args() As [String])

        ' Anzahl der zu erzeugenden Threads
        Const nbThreads As Integer = 3

        ' Initialisierung der Flags
        peutLire = New AutoResetEvent(False)        ' on ne peut pas encore lire
        peutEcrire = New AutoResetEvent(True)        ' on peut déjà écrire

        ' Initialisierung der Synchronisationsvariablen
        lecteur = New Mutex         ' synchronise les lecteurs
        écrivain = New Mutex         ' synchronise les écrivains

        ' Erstellung der Lesethreads
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' Erstellung
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' Start
            lecteurs(i).Start()
        Next i

        ' Erstellung der Schreib-Threads
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' Erstellung
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' Start
            écrivains(i).Start()
        Next i

        'Ende der Hand
        Console.Out.WriteLine("fin de Main...")
    End Sub

    ' Inhalt der Tabelle lesen
    Public Shared Sub lire()
        ' kritischer Abschnitt
        lecteur.WaitOne()        ' un seul lecteur peut passer
        peutLire.WaitOne()        ' on doit pouvoir lire

        ' Tabelle lesen
        Dim i As Integer
        For i = 0 To data.Length - 1
            '1 Sekunde warten
            Thread.Sleep(1000)
            ' Anzeige
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i

        ' kann nicht mehr gelesen werden
        peutLire.Reset()
        ' Schreiben möglich
        peutEcrire.Set()
        ' Ende des kritischen Abschnitts
        lecteur.ReleaseMutex()
    End Sub

    ' In die Tabelle schreiben
    Public Shared Sub écrire()
        ' kritischer Abschnitt
        ' Nur ein Schreiber darf passieren
        écrivain.WaitOne()
        ' Man muss auf die Schreibberechtigung warten
        peutEcrire.WaitOne()

        ' Schreiben in die Tabelle
        Dim i As Integer
        For i = 0 To data.Length - 1
            'Wartezeit 1 s
            Thread.Sleep(1000)
            ' Anzeige
            data(i) = objRandom.Next(0, 1000)
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
        Next i

        ' Man kann nicht mehr schreiben
        peutEcrire.Reset()
        ' Lesen möglich
        peutLire.Set()
        'Ende des kritischen Abschnitts
        écrivain.ReleaseMutex()
    End Sub
End Class

Die Ausführung liefert folgende Ergebnisse:

dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C

Folgende Punkte sind zu beachten:

  • Es gibt tatsächlich jeweils nur einen Leser, obwohl dieser im kritischen Abschnitt lire die CPU-Ressourcen verliert
  • Es gibt tatsächlich jeweils nur einen Schreiber, obwohl dieser im kritischen Abschnitt écrire die Prozessorsteuerung verliert
  • Ein Leser liest nur, wenn es etwas im Array zu lesen gibt
  • Ein Schreiber schreibt erst, wenn das Array vollständig gelesen wurde