Skip to content

8. Ausführungsthreads

8.1. Einführung

Wenn eine Anwendung gestartet wird, läuft sie in einem Ausführungsablauf, der als Thread bezeichnet wird. Die .NET-Klasse, die einen 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 – statische Eigenschaft
gibt den aktuell ausgeführten Thread zurück
Name – Objekteigenschaft
Thread-Name
isAlive – Objekteigenschaft
gibt an, ob der Thread aktiv ist (true) oder nicht (false)
Start – Objektmethode
startet die Ausführung eines Threads
Abort – Objektmethode
beendet die Ausführung eines Threads dauerhaft
Sleep(n) – statische Methode
hält einen Thread für n Millisekunden an
Suspend() – Objektmethode
setzt die Ausführung eines Threads vorübergehend aus
Resume() – Objektmethode
setzt die Ausführung eines angehaltenen Threads fort
Join() – Objektmethode
blockierende Operation – wartet, bis der Thread beendet ist, bevor mit der nächsten Anweisung fortgefahren wird

Sehen wir uns eine einfache Anwendung an, die die Existenz eines Hauptausführungsthreads demonstriert, in dem die Main-Funktion einer Klasse ausgeführt wird:


' use of threads
Imports System
Imports System.Threading
 
Public Module thread1
    Public Sub Main()
        ' init current thread
        Dim main As Thread = Thread.CurrentThread
        ' display
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' we change the name
        main.Name = "main"
        ' check
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' infinite loop
        While True
            ' display
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' temporary shutdown
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Ausgabe:

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 vorangegangene Beispiel veranschaulicht folgende Punkte:

  • Die Main-Funktion läuft in einem Thread
  • wir können über Thread.CurrentThread auf die Eigenschaften dieses Threads zugreifen
  • die Rolle der Sleep-Methode. Hier ruht der Thread, der Main ausführt, zwischen jeder Anzeige 1 Sekunde lang.

8.2. Erstellen von Ausführungsthreads

Es gibt Anwendungen, in denen Codeabschnitte „gleichzeitig“ in verschiedenen Ausführungsthreads ausgeführt werden. Wenn wir sagen, dass Threads gleichzeitig laufen, verwenden wir diesen Begriff oft im weiteren Sinne. Verfügt der Rechner nur über einen Prozessor, was immer noch häufig der Fall ist, teilen sich die Threads diesen Prozessor: Jeder hat 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 den Prozessor hat, nutzt er diesen normalerweise für die gesamte ihm zugewiesene Zeit. Er kann ihn jedoch vorzeitig freigeben:

  • indem er auf ein Ereignis wartet (wait, join, suspend)
  • indem er für einen bestimmten Zeitraum in den Ruhezustand wechselt (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 typische Konstruktion sieht wie folgt aus:

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

Die als Parameter übergebene Funktion „run“ wird ausgeführt, sobald der Thread gestartet wird.

  1. Die Ausführung des Threads T wird durch T.Start() initiiert: Die an den Konstruktor von T übergebene Funktion [run] wird dann vom Thread T ausgeführt. Das Programm, das die Anweisung T.Start() ausführt, wartet nicht auf den Abschluss der Aufgabe T: Es fährt sofort mit der nächsten Anweisung fort. Wir haben nun zwei Aufgaben, die parallel laufen. Sie müssen oft miteinander kommunizieren, um den Status der gemeinsam zu erledigenden Arbeit zu erfahren. Dies ist das Problem der Thread-Synchronisation.
  1. Einmal gestartet, läuft der Thread autonom. Er stoppt, sobald die Startfunktion, die er ausführt, ihre Arbeit beendet hat.
  1. Wir können bestimmte Signale an die Aufgabe T senden:
    1. T.Suspend() weist ihn an, vorübergehend anzuhalten
    2. T.Resume() weist ihn an, seine Arbeit fortzusetzen
    3. T.Abort() weist ihn an, dauerhaft zu stoppen
  1. Sie können auch mit T.join() darauf warten, dass die Ausführung abgeschlossen ist. Dies ist eine blockierende Anweisung: Das Programm, das sie ausführt, wird blockiert, bis die Aufgabe T ihre Arbeit beendet hat. Es handelt sich um ein Mittel zur Synchronisation.

Betrachten wir das folgende Programm:


' options
Option Strict On
Option Explicit On 
 
' namespaces
Imports System
Imports System.Threading
 
Module thread2
    Public Sub Main()
        ' init Current thread
        Dim main As Thread = Thread.CurrentThread
        ' name the Thread
        main.Name = "main"
 
        ' creation of execution threads
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' create thread i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' set the thread name
            tâches(i).Name = "tache_" & i
            ' start execution of thread i
            tâches(i).Start()
        Next i
        ' end of hand
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub
 
    Public Sub affiche()
        ' display start of execution
        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")))
        ' sleep for 1 s
        Thread.Sleep(1000)
        ' display end of run
        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 Main-Funktion ausführt, erstellt 5 weitere Threads, die für die Ausführung der statischen Methode display zuständig sind. Die Ergebnisse lauten 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 sehen wir, dass der Start der Ausführung eines Threads nicht blockierend ist. Die Main-Methode startete die Ausführung von 5 Threads parallel und beendete die Ausführung vor ihnen. Die
            ' on lance l'exécution du thread i
            tâches(i).Start()

startet die Ausführung des Threads tasks[i], doch sobald dies geschehen ist, wird die Ausführung sofort mit der nächsten Anweisung fortgesetzt, ohne auf den Abschluss des Threads zu warten.

  • Alle erstellten Threads müssen die Methode display ausführen. Die Reihenfolge der Ausführung ist unvorhersehbar. Auch wenn es im Beispiel so aussieht, als würde die Reihenfolge der Ausführung der Reihenfolge der Ausführungsanforderungen folgen, lassen sich daraus keine allgemeinen Schlussfolgerungen ziehen. Das Betriebssystem verfügt hier über 6 Threads und einen Prozessor. Es wird den Prozessor diesen 6 Threads nach seinen eigenen Regeln zuweisen.
  • Die Ergebnisse zeigen einen Effekt der Sleep-Methode. Im Beispiel führt Thread 0 als erster die display-Methode aus. Die Meldung zum Start der Ausführung wird angezeigt, dann führt er die Sleep-Methode aus, die ihn für 1 Sekunde anhält. Er verliert daraufhin den Prozessor, der für einen anderen Thread verfügbar wird. Das Beispiel zeigt, dass Thread 1 ihn erhält. Thread 1 wird denselben Weg einschlagen, ebenso wie die anderen Threads. Wenn die 1-sekündige Sleep-Pause für Thread 0 endet, kann dessen Ausführung fortgesetzt werden. Das System weist ihm den Prozessor zu, und er kann die Ausführung der display-Methode abschließen.

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

        ' fin de main
        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 Main-Funktion erstellten Threads werden nicht ausgeführt. Es ist die Anweisung

        Environment.Exit(0)

, die dies bewirkt: Sie beendet alle Threads in der Anwendung, nicht nur den Main-Thread. Die Lösung für dieses Problem besteht darin, dass die Main-Methode wartet, bis die von ihr erstellten Threads ihre Ausführung beendet haben, bevor sie sich selbst beendet. Dies kann mithilfe der Join-Methode der Thread-Klasse erreicht werden:


        ' on attend la fin d'exécution de tous les threads
        For i = 0 To tâches.Length - 1
            ' attente de la fin d'exécution du thread i
            tâches(i).Join()
        Next i        'for
        ' fin de main
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Dies führt zu folgenden Ergebnissen:

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. Vorteile von Threads

Nachdem wir nun die Existenz eines Standard-Threads – derjenige, der die Main-Methode ausführt – hervorgehoben haben und wissen, wie man weitere erstellt, wollen wir uns die Vorteile von Threads für uns ansehen und erklären, warum wir sie hier vorstellen. Es gibt eine Art von Anwendung, die sich besonders gut für die Verwendung von Threads eignet: Client-Server-Anwendungen im Internet. In einer solchen Anwendung beantwortet ein Server auf dem Rechner S1 Anfragen von Clients auf den entfernten Rechnern C1, C2, ..., Cn.

Wir nutzen täglich Internetanwendungen, die diesem Muster folgen: Webdienste, E-Mail, das Durchsuchen von Foren, Dateiübertragungen... In der obigen Abbildung muss der Server S1 die Clients C1, C2, ..., Cn gleichzeitig bedienen. Nehmen wir das Beispiel eines FTP-Servers (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 monopolisiert. Üblicherweise erstellt der Server so viele Ausführungsthreads, wie es Clients gibt. Jeder Thread ist dann für die Betreuung eines bestimmten Clients zuständig. Da der Prozessor zyklisch unter allen aktiven Threads auf dem Rechner aufgeteilt wird, verbringt der Server mit jedem Client nur wenig Zeit 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 müssen Threads unter Umständen zusammenarbeiten, um ihren Client den angeforderten Dienst bereitzustellen, insbesondere beim Zugriff auf gemeinsam genutzte Ressourcen. Das obige Diagramm ähnelt den Schaltern einer großen Behörde, wie beispielsweise einer Poststelle, wo ein Mitarbeiter an jedem Schalter einen Kunden bedient. Angenommen, diese Mitarbeiter müssen von Zeit zu Zeit Kopien von Dokumenten anfertigen, die ihre Kunden mitgebracht haben, und es gibt nur einen Kopierer. 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. Betrachten Sie das folgende Beispiel:

  • Eine Anwendung generiert n Threads, wobei n als Parameter übergeben wird
  • Die gemeinsam genutzte Ressource ist ein Zähler, der von jedem generierten Thread erhöht werden muss
  • am Ende der Anwendung wird der Wert des Zählers angezeigt. Wir sollten daher n erhalten.

Das Programm sieht wie folgt aus:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread3
    ' class variables
    Private Shared cptrThreads As Integer = 0
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' error
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' end
            Environment.Exit(2)
        End Try
        ' thread creation and generation
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        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 die Erstellung des Threads eingehen, da wir dies bereits behandelt haben. Konzentrieren wir uns stattdessen auf die Increment-Methode, die von jedem Thread verwendet wird, um den statischen Zähler cptrThreads zu inkrementieren.

  1. Der Zähler wird gelesen
  2. , der Thread pausiert für 1 Sekunde. Er verliert somit die CPU
  3. wird der Zähler erhöht

Schritt 2 dient lediglich dazu, den Thread zu zwingen, die CPU abzugeben. Die CPU wird an einen anderen Thread übergeben. In der Praxis gibt es keine Garantie dafür, dass ein Thread nicht zwischen dem Zeitpunkt des Auslesens des Zählers und dem Zeitpunkt des Inkrementierens unterbrochen wird. Es besteht das Risiko, die CPU zwischen dem Auslesen des Zählerwerts und dem Schreiben des um 1 erhöhten Werts zu verlieren. Tatsächlich umfasst die Inkrementierung mehrere grundlegende Befehle auf Prozessorebene, die unterbrochen werden können. Schritt 2, die einsekündige Pause, dient daher lediglich dazu, diesem Risiko Rechnung zu tragen. 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

Wenn man sich diese Ergebnisse ansieht, wird klar, was hier vor sich geht:

  • Ein erster Thread liest den Zähler aus. Er findet den Wert 0.
  • Er pausiert für 1 Sekunde und gibt damit die CPU frei
  • Ein zweiter Thread übernimmt dann die CPU und liest ebenfalls den Zählerwert. Dieser ist immer noch 0, da der vorherige Thread ihn noch nicht erhöht hat. Auch er pausiert für 1 Sekunde.
  • Innerhalb von 1 Sekunde haben alle 5 Threads Zeit, zu laufen und den Wert 0 zu lesen.
  • Wenn sie nacheinander wieder aktiv werden, erhöhen sie den gelesenen Wert 0 und schreiben den Wert 1 in den Zähler, was vom Hauptprogramm (Main) bestätigt wird.

Wo liegt das Problem? Der zweite Thread hat einen falschen Wert gelesen, weil der erste Thread unterbrochen wurde, bevor er seine Aufgabe – die Aktualisierung des Zählers im Fenster – beendet hatte. Dies führt uns zum Konzept der kritischen Ressourcen und kritischen Abschnitte in einem Programm:

  • Eine kritische Ressource ist eine Ressource, die jeweils nur von einem Thread gehalten werden kann. Hier ist die kritische Ressource der Zähler.
  • Ein kritischer Abschnitt eines Programms ist eine Folge von Anweisungen im Ausführungsablauf eines Threads, während der dieser auf eine kritische Ressource zugreift. Wir müssen sicherstellen, 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, der sich zwischen dem Auslesen des Zählers und dem Schreiben seines neuen Werts befindet:


        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1

Um diesen Code auszuführen, muss sichergestellt sein, dass ein Thread allein ist. Er darf zwar unterbrochen werden, aber während dieser Unterbrechung darf kein anderer Thread denselben Code ausführen können. Die .NET-Plattform bietet mehrere Werkzeuge, um den Single-Thread-Zugriff auf kritische Codeabschnitte zu gewährleisten. Wir verwenden die Mutex-Klasse:

Image

Hier verwenden wir nur die folgenden Konstruktoren und Methoden:

public Mutex()
erstellt ein Synchronisationsobjekt M
public bool WaitOne()
Der Thread T1, der die Operation M.WaitOne() ausführt, fordert die Kontrolle über das Synchronisationsobjekt M an. Wenn der Mutex M von keinem Thread gehalten wird (der Ausgangszustand), wird er dem Thread T1, der ihn angefordert hat, „zugewiesen“. Wenn wenig später der Thread T2 denselben Vorgang ausführt, wird er blockiert. Tatsächlich kann ein Mutex nur einem Thread gehören. Er wird freigegeben, sobald der Thread T1 den von ihm gehaltenen Mutex M freigibt. Mehrere Threads können somit blockiert werden, während sie auf den Mutex M warten.
public void
ReleaseMutex()
Der Thread T1, der die Operation M.ReleaseMutex() ausführt, gibt die Kontrolle über den Mutex M ab. Wenn Thread T1 den Prozessor verliert, kann das System ihn einem der Threads zuweisen, die auf den Mutex M warten. 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() frei. Ein kritischer Codeabschnitt, der jeweils nur von einem Thread ausgeführt werden darf, ist eine gemeinsam genutzte Ressource. Die Synchronisation der Ausführung des kritischen Abschnitts kann wie folgt erreicht werden:

M.WaitOne()
' le thread est seul à entrer ici
' section critique
....
M.ReleaseMutex()

wobei M ein Mutex-Objekt ist. Natürlich darf man niemals vergessen, einen nicht mehr benötigten Mutex freizugeben, damit ein anderer Thread den kritischen Abschnitt betreten kann; andernfalls erhalten Threads, die auf einen Mutex warten, der nie freigegeben wird, niemals Zugriff auf den Prozessor. Darüber hinaus muss man eine Deadlock-Situation vermeiden, in der zwei Threads aufeinander warten. Betrachten Sie die folgenden Aktionen, die nacheinander ablaufen:

  • Ein Thread T1 erwirbt die Kontrolle über einen Mutex M1, um auf eine gemeinsam genutzte Ressource R1 zuzugreifen
  • Ein Thread T2 erwirbt einen Mutex M2, um auf eine gemeinsam genutzte Ressource R2 zuzugreifen
  • Thread T1 fordert Mutex M2 an. Er wird blockiert.
  • Thread T2 fordert Mutex M1 an. Er wird blockiert.

Hier warten die Threads T1 und T2 aufeinander. Diese Situation tritt auf, wenn Threads zwei gemeinsam genutzte Ressourcen benötigen: die Ressource R1, die von Mutex M1 kontrolliert wird, und die Ressource R2, die von Mutex M2 kontrolliert wird. Eine mögliche Lösung besteht darin, beide Ressourcen gleichzeitig mithilfe eines einzigen Mutex M zu erwerben. Dies ist jedoch nicht immer machbar, wenn es beispielsweise zu einer längeren Sperre einer ressourcenintensiven Ressource führt. Eine andere Lösung besteht darin, dass ein Thread, der M1 hält und M2 nicht erhalten kann, M1 freigibt, um ein Deadlock zu vermeiden. Wenn wir das soeben Gesehene auf das vorherige Beispiel anwenden, sieht unsere Anwendung wie folgt aus:


' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class thread4
    ' class variables
    Private Shared cptrThreads As Integer = 0    ' thread counter
    Private Shared autorisation As Mutex
 
    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' instructions for use
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100
 
        ' verification no. of arguments
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' stop
            Environment.Exit(1)
        End If
        ' argument quality check
        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
 
        ' initialize access authorization to a critical section
        autorisation = New Mutex
 
        ' thread creation and generation
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naming
            threads(i).Name = "tache_" & i
            ' launch
            threads(i).Start()
        Next i
        ' waiting for threads to finish
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' counter display
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub
 
    Public Shared Sub incrémente()
        ' increases thread counter
        ' we request permission to enter the critical secton
        autorisation.WaitOne()
        ' meter reading
        Dim valeur As Integer = cptrThreads
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' waiting
        Thread.Sleep(1000)
        ' counter incrementation
        cptrThreads = valeur + 1
        ' follow-up
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' access authorization is returned
        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. Ereignisbasierte Synchronisation

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

  1. Wir haben ein Array, in das einige Prozesse Daten schreiben (die Produzenten) und andere sie lesen (die Konsumenten).
  2. Die Produzenten sind einander gleich, schließen sich jedoch gegenseitig aus: Es kann jeweils nur ein Produzent Daten in das Array schreiben.
  3. Die Konsumenten sind einander gleich, schließen sich jedoch gegenseitig aus: Es kann jeweils nur ein Leser die im Array gespeicherten Daten lesen.
  4. Ein Konsument kann Daten erst dann aus dem Array lesen, wenn ein Produzent sie dort geschrieben hat, und ein Produzent kann erst dann neue Daten in das Array schreiben, wenn die vorhandenen Daten bereits gelesen wurden.

In dieser Erklärung lassen sich zwei gemeinsam genutzte Ressourcen unterscheiden:

    1. die beschreibbare Tabelle
    2. das schreibgeschützte Array

Der Zugriff auf diese beiden gemeinsam genutzten Ressourcen kann, wie zuvor gezeigt, durch Mutexe gesteuert werden, wobei für jede Ressource ein Mutex verwendet wird. Sobald ein Konsument das schreibgeschützte Array erhalten hat, muss er überprüfen, ob es tatsächlich Daten enthält. Dazu wird ein Ereignis verwendet, um ihn darüber zu informieren. Ebenso muss ein Produzent, der das schreibgeschützte Array erhalten hat, warten, bis ein Konsument es geleert hat. Auch hier wird ein Ereignis verwendet.

Die verwendeten Ereignisse gehören zur Klasse „AutoResetEvent“:

Image

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

while(peutEcrire==false)        ' attente active

oder

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

In der ersten Methode blockiert der Thread den Prozessor unnötig. In der zweiten überprüft er alle 100 ms den Status des booleschen Werts canWrite. Die Klasse AutoResetEvent ermöglicht eine weitere Verbesserung: Der Thread fordert an, geweckt zu werden, sobald das Ereignis eintritt, auf das er wartet:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' thread waits for peutEcrire event to change to true

Die Operation

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

initialisiert die boolesche Variable canWrite 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 Thread fortfährt, wenn der Boolesche Wert canWrite* auf „true“ gesetzt ist; andernfalls wird er blockiert, bis der Wert auf „true“ gesetzt wird. Ein anderer Thread setzt ihn mithilfe der Operation canWrite.Set() auf „true“ oder mithilfe der Operation canWrite.Reset()* auf „false“.

Das Produzent-Konsument-Programm lautet wie folgt:


' use of reader and writer threads
' illustrates the simultaneous use of shared resources and synchronization
 
' options
Option Explicit On 
Option Strict On
 
' use of threads
Imports System
Imports System.Threading
 
Public Class lececr
 
    ' class variables
    Private Shared data(5) As Integer    ' resource shared between reader and writer threads
    Private Shared lecteur As Mutex    ' synchronization variable to read the table
    Private Shared écrivain As Mutex    ' synchronization variable to write to the table
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' a random number generator
    Private Shared peutLire As AutoResetEvent    ' indicates that you can read the contents of data
    Private Shared peutEcrire As AutoResetEvent
 
    Public Shared Sub Main(ByVal args() As [String])
 
        ' number of threads to generate
        Const nbThreads As Integer = 3
 
        ' flag initialization
        peutLire = New AutoResetEvent(False)        ' cannot be read yet
        peutEcrire = New AutoResetEvent(True)        ' we can already write
 
        ' initialization of synchronization variables
        lecteur = New Mutex         ' synchronizes drives
        écrivain = New Mutex         ' synchronizes writers
 
        ' creation of reader threads
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creation
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' launch
            lecteurs(i).Start()
        Next i
 
        ' creating writer threads
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' creation
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' launch
            écrivains(i).Start()
        Next i
 
        'end of hand
        Console.Out.WriteLine("fin de Main...")
    End Sub
 
    ' read the contents of the table
    Public Shared Sub lire()
        ' review section
        lecteur.WaitOne()        ' a single reader can pass
        peutLire.WaitOne()        ' you must be able to read
 
        ' table reading
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i
 
        ' we can no longer read
        peutLire.Reset()
        ' we can write
        peutEcrire.Set()
        ' end of critical section
        lecteur.ReleaseMutex()
    End Sub
 
    ' write in the table
    Public Shared Sub écrire()
        ' review section
        ' only one writer can pass
        écrivain.WaitOne()
        ' we have to wait for write authorization
        peutEcrire.WaitOne()
 
        ' writing table
        Dim i As Integer
        For i = 0 To data.Length - 1
            'wait 1 s
            Thread.Sleep(1000)
            ' display
            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
 
        ' we can no longer write
        peutEcrire.Reset()
        ' you can read
        peutLire.Set()
        'end of critical section
        é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, auch wenn dieser die CPU im kritischen Leseabschnitt verliert
  • Es gibt tatsächlich jeweils nur einen Schreiber, auch wenn dieser im kritischen Schreibabschnitt die CPU verliert
  • Ein Leser liest nur, wenn es in der Tabelle etwas zu lesen gibt
  • Ein Schreiber schreibt nur, wenn das Array vollständig gelesen wurde