Dieses Kapitel widmet sich dem Zusammenspiel gleichzeitig ablaufender Programme. Insbesondere der gemeinsame Zugriff auf dieselben Daten muss geregelt werden.
5 Multithreading
In diesem Kapitel gehe ich auf den Themenkreis ein, der mit dem letzten verbliebenen Schlüsselwort von Visual Basic, SyncLock, zusammenhängt (die LINQ-spezifischen Wörter im nächsten Kapitel sind keine echten Schlüsselwörter). Bevor wir es einsetzen können, muss erst dieser Themenkreis erläutert werden. In Abschnitt 3.9.4, »Asynchrone Aufrufe«, haben wir bereits erste Schritte unternommen, um mehrere Aktionen parallel zu bearbeiten. Dieser Abschnitt beschäftigt sich nun mit der geregelten Zusammenarbeit der parallel ablaufenden Aktivitäten. Jede wird in einem sogenannten Thread (englisch: Faden) verwaltet. Er stellt die Schnittstelle zur Verfügung, damit das Betriebssystem einem Programmteil Hardware zuweisen (oder entziehen) kann. Jede Anwendung enthält einen oder mehrere Threads. Sind es mehrere, spricht man von Multithreading.
Für die hier beschriebenen Prinzipien spielt es keine Rolle, ob die verschiedenen Programmteile auf verschiedenen Prozessoren laufen oder nur auf einem einzelnen – in einem Restaurant ist es Ihnen ja auch einerlei, wer die leckere Mahlzeit kocht. In der Praxis können sich dadurch Unterschiede in der Laufzeit ergeben, dass bei mehreren Aktionen auf demselben Prozessor eine »parallele« Ausführung dadurch simuliert wird, dass zwischen ihnen schnell hin und her geschaltet wird. Koordiniert wird der Vorgang durch ein Programm des Betriebssystems namens Scheduler. Nach jedem Schaltvorgang wird der Prozessor eine kurze Zeit für einen Programmteil reserviert. Die Zeiten für die Umschaltung machen sich in der Praxis nur dann bemerkbar, wenn zwischen extrem vielen kleinen Einheiten sehr oft umgeschaltet werden muss. Außer der Ausführungszeit gibt es keine Effekte durch unterschiedliche Prozessorzahlen. Selbst der gleichzeitige Zugriff von zwei Prozessoren auf dieselben Daten ist nicht wirklich unterschiedlich, da die Umschaltung ja auch während eines Datenzugriffs erfolgen kann und daher der Datenzugriff sowieso genau geregelt werden muss.
Hinweis |
Mit dem Ende des letzten Threads einer Anwendung wird diese auch beendet. |
Da die Verwendung mehrerer Threads Ressourcen verbraucht und die Umschaltung zwischen den Threads auch ein wenig Zeit kostet, sollten Sie nicht jede kleine Aufgabe in einen eigenen Thread verpacken. Außerdem wird die Fehlersuche in einer Anwendung mit mehreren Threads schnell aufwendig. Es gibt aber auch gute Gründe dafür, einen neuen Thread für eine Aufgabe zu nutzen:
- Der normale Programmfluss hat mit der Aufgabe nichts zu tun, zum Beispiel das Abspielen von Hintergrundmusik.
- Etwas muss vorrangig ausgeführt werden, zum Beispiel die Maussteuerung.
- Eine aufwendige Aufgabe soll andere Programmteile nicht blockieren, zum Beispiel das Laden eines Videos.
- Eine Aktivität soll von außen kontrollierbar sein, zum Beispiel um sie abzubrechen.
In den folgenden Abschnitten erläutere ich zuerst, wie ein Thread gestartet wird und welche Zustände er annehmen kann. Dann widme ich mich dem Zusammenspiel mit anderen Threads und dem Datenaustausch zwischen verschiedenen Threads. Der Aufruf parallel ablaufender Methoden und eine kurze Einführung in Threadpools bilden den Abschluss.
5.1 Start eines Threads 

Nur Programmteile, die einem Thread zugeordnet sind, können ausgeführt werden, da der Thread die Schnittstelle zum Betriebssystem darstellt und dieses die volle Kontrolle über alle Prozesse haben muss. Bisher haben Sie in den meisten Programmen nichts davon gemerkt, da der Start des Hauptthreads zusammen mit dem Start der Anwendung erfolgt ist. Hier starten wir Programmteile explizit in einem anderen als dem Hauptthread. Damit ein Programmteil von den anderen losgelöst gestartet werden kann, müssen zwei Schritte erfolgen:
- Ein Objekt vom Typ Thread muss instanziiert werden.
- Die Methode Start() dieses Objekts startet den Programmteil.
Alle Konstruktoren von Thread erwarten als ersten Parameter die Adresse einer Methode, die in einem Delegate gekapselt ist (siehe Abschnitt 3.9.1, »Funktionszeiger: Delegate«). Alle diese Delegates haben keinen Rückgabewert. In einem optionalen zweiten Parameter können Sie die Maximalgröße des Stacks festlegen (ab Windows XP). Neben der Verwaltungsinformation für sämtliche Methodenaufrufe werden auf dem Stack auch alle lokalen Variablen abgelegt. Wenn diese in Ihren Programmen viel Speicher belegen oder Sie viel Gebrauch von Rekursion machen, kann es notwendig sein, die Standardgröße des Stacks von 1 MB zu erhöhen. Sie sollten aber nicht zu großzügig Platz reservieren.
Hinweis |
Ein einmal beendeter Thread kann nicht wieder gestartet werden. |
5.1.1 Parameterloser Start 

Die einfachste Art, eine unabhängige Aktivität zu starten, verwendet keine Parameter bei deren Start.
Public NotInheritable Delegate Sub ThreadStart() |
Das folgende Codefragment definiert eine Methode zum Ausdruck von Zeichen. Durch die Inkrementierung der klassengebundenen Variablen no wird jeder Aufruf ein anderes Zeichen ausgeben. Dies ermöglicht es uns, verschiedene Aufrufe zu unterscheiden. Die Methode wird sowohl über die Methode Start() eines neu erstellten Threads gestartet als auch direkt vom Hauptthread aus. Die erste Variante leiert den Start nur an. Die Abarbeitung erfolgt in einem anderen Thread, sodass der Aufruf Start() fast keine Zeit benötigt und mit der Programmausführung fortgefahren werden kann. Es laufen also zwei Versionen der Methode Druck() in unterschiedlichen Threads, die mit Console.Write() in dasselbe Ausgabefenster schreiben.
'...\Multitasking\Start\Parameterlos.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Parameterlos
Dim no As Integer = 0
Sub Druck()
no += 1
Dim z As Char = ChrW(41 + no)
For no As Integer = 0 To 200
Console.Write(z)
Next
End Sub
Sub Test()
Dim start As New ThreadStart(AddressOf Druck)
Dim einheit As New Thread(start)
einheit.Start()
Druck()
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe zeigt, wie beide Threads abwechselnd zum Zuge kommen. Die Größe und Anzahl der Abschnitte sowie deren Reihenfolge im Ausdruck ist nicht festgelegt. Jeder neue Lauf des Programms kann ein neues Muster erzeugen. Sollten bei Ihnen die Pluszeichen immer nach den Sternen erscheinen, ist Ihr Rechner »zu« schnell, und Sie sollten den Schleifenzähler größer als 200 wählen. So erzwingen Sie eine Ausführungszeit, die größer als die Zeiteinheit ist, in denen das Betriebssystem einen Thread jeweils rechnen lässt.
****************************+++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++***********************************************************************
***************************************************************************
***************************
Diese Unvorhersagbarkeit der Ausgabe zeigt auch das Hauptproblem beim Multithreading: Es laufen mehrere Aktionen parallel, die sich ohne Weiteres ins Gehege kommen können.
5.1.2 Start mit Parameter 

Beim vorigen Beispiel war die Implementierung zur Unterscheidung der Threads etwas unbeholfen. Leichter wird es, wenn dem Thread beim Start ein Parameter mitgegeben werden kann. Dazu akzeptiert ein weiterer Konstruktor der Klasse Thread ein anderes Delegate als ersten Parameter:
Public NotInheritable Delegate Sub ParameterizedThreadStart(obj As Object) |
Damit darf die Methode Druck() nun einen Parameter haben. Im folgenden Codefragment übergeben wir als Objekte zwei verschiedene Zahlen. Die Ausgabe ist qualitativ dieselbe wie im parameterlosen Fall.
'...\ Multitasking\Start\Parameter.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Parameter
Sub Druck(ByVal z As Object)
For no As Integer = 0 To 200
Console.Write(z)
Next
End Sub
Sub Test()
Dim start As New ParameterizedThreadStart(AddressOf Druck)
Dim einheit As New Thread(start)
'kurz: Dim einheit As New Thread(AddressOf Druck)
einheit.Start(1)
Druck(2)
Console.ReadLine()
End Sub
End Module
End Namespace
Hinweis |
Vergessen Sie, einen Parameter an Start() zu übergeben, wird der Wert Nothing übergeben. |
5.1.3 Hintergrundthreads 

.NET unterscheidet Vorder- und Hintergrundthreads. Wie der Name vermuten lässt, sind Letztere nicht zur direkten Kommunikation mit dem Benutzer ausgelegt, sondern arbeiten im Verborgenen. Daher ist die Lebensdauer einer Anwendung unabhängig von Hintergrundthreads. Egal wie viele davon noch laufen, mit dem Ende des letzten Vordergrundthreads wird auch die Anwendung beendet. Die Eigenschaft IsBackground legt fest, ob ein Thread im Hintergrund läuft. Im nächsten Codefragment wird in einem Hintergrundthread die Schleife in Druck() neunmal durchlaufen und im Hauptthread, der immer ein Vordergrundthread ist, zweimal. Die Schleife wird durch den Aufruf von Sleep() künstlich um eine Sekunde verzögert, damit Sie den Programmlauf leichter am Bildschirm verfolgen können. Dies ist wichtig, weil das Programm endet, ohne das Konsolenfenster offen zu halten. Die Methode Sleep() wird in Abschnitt 5.2, »Zusammenspiel«, genauer erläutert.
'...\ Multitasking\Start\Hintergrund.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Hintergrund
Sub Druck(ByVal z As Object)
For no As Integer = 0 To CType(z, Integer)
Console.Write(z) : Thread.Sleep(1000)
Next
Console.WriteLine("**")
End Sub
Sub Test()
Dim einheit As New Thread(AddressOf Druck)
einheit.IsBackground = True
einheit.Start(9) 'Hintergrund : 9 Schleifendurchläufe
Druck(2) 'Vordergrund : 2 Schleifendurchläufe
End Sub
End Module
End Namespace
Hinweis |
Mit ReadLine() am Programmende wird der Vordergrundthread am Leben erhalten, und der Effekt ist nicht zu sehen. |
Einige Details in den Unterschieden von Vorder- und Hintergrundprozess sind betriebssystemabhängig. Auf einem Desktopbetriebssystem läuft ein Hintergrundprozess in der Regel langsamer als ein Vordergrundprozess, und die zugeteilten Zeitscheiben sind kürzer. Auf einem Serverbetriebssystem ist es umgekehrt.
5.1.4 Threadeigenschaften 

Jeder Thread hat auch ein paar Eigenschaften, von denen ich hier einige herausgreife. Die vollständige Liste ist in Abschnitt 5.2.8, »Die Klasse Thread«, abgedruckt. Die klassengebundene Eigenschaft CurrentThread liefert eine Referenz auf den Thread, der die Anweisung gerade ausführt. Somit haben Sie die Möglichkeit, eine Referenz auf den Hauptthread einer Anwendung zu erhalten. Bei der Fehlersuche hilft die Benennung durch Name. Das folgende Codefragment zeigt diese Eigenschaften im Einsatz.
'...\ Multitasking\Start\Eigenschaften.vb |
Option Strict On
Imports System.Threading
Namespace Multitasking
Module Eigenschaften
Sub Test()
Dim th As Thread = Thread.CurrentThread
Console.WriteLine("Hauptthread hat die ID {0} und den Namen ""{1}""", _
th.ManagedThreadId, th.Name)
th.Name = "Haupt"
Console.WriteLine("Thread {0} verwendet die Ländereinstellung {1}", _
th.Name, th.CurrentCulture)
Console.ReadLine()
End Sub
End Module
End Namespace
Sowohl die ID als auch die Ländereinstellung können bei Ihnen andere Werte haben.
Hauptthread hat die ID 9 und den Namen ""
Thread Haupt verwendet die Ländereinstellung en-US
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.