1. Einführung
Multithreading ist wie die parallele Arbeit mehrerer Kollegen im Büro: einer druckt Dokumente, ein anderer ruft einen Kunden an, der dritte kocht Kaffee (und natürlich sind das alles Programmierer). Wenn wir nur einen Mitarbeiter hätten, würde er alles nacheinander machen, und das Büro würde vor Langeweile und Kaffeeschlangen ersticken. In der Programmierung ist die Situation ähnlich: ein single-threaded Programm kann jeweils nur eine Aufgabe ausführen.
Stell dir vor, unsere Anwendung führt eine langwierige Operation aus, zum Beispiel lädt eine Datei aus dem Internet oder berechnet eine riesige Tabelle. Alles andere ist in diesem Moment "eingefroren" — Buttons reagieren nicht, Animationen stoppen, die Worte "nicht antwortet" erscheinen im Popup.
Multithreading ermöglicht es der Anwendung, mehrere Dinge gleichzeitig zu tun: die UI bleibt responsiv, Operationen laufen parallel und wir müssen nicht in einen Monolog verfallen wie "Computer, du hängst schon wieder?!".
Grundbegriffe und Terminologie
Bevor wir eintauchen, klären wir, was ein Thread ist und worin er sich von einem Prozess unterscheidet.
- Prozess (Process): Ein eigenständiges Programm mit eigenem Adressraum, Variablen und Ressourcen. Zum Beispiel ist jede gestartete Anwendung in Windows ein eigener Prozess.
- Thread (Thread): Eine Ausführungseinheit innerhalb eines Prozesses. Ein Prozess kann einen oder mehrere Threads enthalten, die dieselben Ressourcen (Speicher, Variablen) teilen.
Warum wirft Multithreading so viele Fragen auf?
Weil Threads wilde Typen sind: sie können jederzeit mit der Ausführung beginnen, Daten durcheinanderbringen, sich gegenseitig unterbrechen und im Speicher Chaos anrichten, wenn man nicht auf die Reihenfolge achtet. Wenn es sich anfühlt wie ein Kindergarten ohne Aufsicht — genau so ist es! Disziplin und Sorgfalt im Umgang mit Threads sind die Grundlage für zuverlässige Multithread-Programme.
2. Geschichte und Rolle von Multithreading in C# und .NET
Früher waren C#-Programme oft single-threaded und einfach. Mit steigenden Performance-Anforderungen, dem Aufkommen von Multicore-CPUs und dem Bedarf an responsiven Anwendungen ohne UI-Hänger kamen in .NET Mittel für Multithreading hinzu. Zuerst gab es die klassische System.Threading.Thread-Klasse, später erschienen Tasks (Task), async-Methoden (async/await), parallele Datenverarbeitung (PLINQ) und höherstufige Synchronisationsprimitive.
C# ist zu einer mächtigen Plattform herangewachsen, wo Multithreading keine Seltenheit, sondern Alltag ist.
Visuell: Prozess und Threads
Hier eine einfache Schema:
+--------------------------------------------------+
| Prozess (dein Programm) |
| +-------------+ +-------------+ |
| | Thread 1 | | Thread 2 | |
| +-------------+ +-------------+ |
| ... |
| +-------------+ |
| | Thread N | |
| +-------------+ |
+--------------------------------------------------+
Alle Threads innerhalb eines Prozesses sehen gemeinsame Variablen und Ressourcen.
3. Wie erstellt man einen Thread in C#?
Fangen wir mit dem einfachsten an: der Thread-Klasse aus dem Namespace System.Threading.
Beispiel: Einen zweiten Thread starten
Nehmen wir an, wir haben eine lang laufende Aufgabe — z.B. die Summe der Zahlen von 1 bis 10_000_000. Während gerechnet wird, soll der Hauptthread eine Begrüßung ausgeben.
using System;
using System.Threading;
class Program
{
// Methode für die zweite Aufgabe
static void CalculateSum()
{
long sum = 0;
for (int i = 1; i <= 10_000_000; i++)
sum += i;
Console.WriteLine($"[Thread 2] Summe: {sum}");
}
static void Main()
{
// Wir erstellen einen Thread und geben das Delegat an die Methode
Thread thread = new Thread(CalculateSum);
thread.Start(); // Wir starten den zweiten Thread
// Der Hauptthread macht weiter
Console.WriteLine("[Thread 1] Hallo! Wir arbeiten parallel...");
// Wir warten auf das Ende des zweiten Threads vor dem Beenden
thread.Join();
Console.WriteLine("[Thread 1] Alles fertig!");
}
}
Was passiert?
Auf dem Bildschirm erscheint die Zeile "[Thread 1] Hallo! Wir arbeiten parallel...", und dann, wenn der Rechen-Thread fertig ist, wird die Summe ausgegeben.
Typisches Problem: Wer zuerst schreibt, gewinnt
Probiere, den Code mehrmals auszuführen — die Reihenfolge der Ausgaben kann unterschiedlich sein! Manchmal erscheint die Summe zuerst, manchmal die Begrüßung. Das ist echte Multithreading-Wirkung — Programme werden weniger deterministisch, wie die Laune einer Katze am Montag.
4. Speicherbereich: was sehen Threads?
Alle Threads innerhalb eines Prozesses haben Zugriff auf dieselben Variablen (sofern sie nicht lokal für eine Methode sind). Wenn eine Variable in einem Thread geändert wird, sehen die anderen Threads die Änderung!
Beispiel: Gemeinsame Variable
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 1000; i++)
counter++;
}
static void Main()
{
Thread t1 = new Thread(Increment);
Thread t2 = new Thread(Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"counter = {counter}");
}
}
Welche Zahl erwarten wir für counter? Logisch wäre 2000, weil jeder Thread 1000 mal erhöht.
Aber nein! Starte das Programm mehrmals — du wirst unterschiedliche Werte sehen: 1782, 1935, 1999…
Warum? Das ist das klassische Problem der Race Condition — Threads "fangen" einander zwischen Lesen -> Erhöhen -> Schreiben, und einige Inkremente gehen verloren.
5. Nützliche Feinheiten
Wie interagieren Threads mit der UI?
In modernen Desktop-Anwendungen (WinForms/WPF/MAUI) bedient der Hauptthread die GUI. Alle Benutzeraktionen (Klicks, Eingaben) laufen in diesem Thread. Hintergrundaufgaben sollten in anderen Threads ausgeführt werden, aber per Regel darf man die UI nicht direkt aus anderen Threads "anfassen". Das verhindert Chaos.
In der Konsole gibt es diese Einschränkung nicht, man kann aus jedem Thread in Console.WriteLine schreiben. Aber in echten Anwendungen kann ohne richtige Synchronisation die UI "aus dem Tritt geraten".
Sequenz und Parallelität
Eine Tabelle, um die Unterschiede zu festigen.
| Single-threaded Code | Multithreaded Code |
|---|---|
| Führt Aufgaben nacheinander aus | Aufgaben können gleichzeitig ausgeführt werden |
| UI "hängt" bei langen Aufgaben | UI bleibt responsiv |
| Variablen einfach lesen und schreiben | Erfordert Kontrolle über den Datenzugriff |
| Einfach zu debuggen | Kann schwierig zu debuggen sein |
Wichtige Punkte beim Arbeiten mit Threads
- Gemeinsame Variablen — gemeinsames Risiko. Wie oben gezeigt, wenn mehrere Threads eine gemeinsame Variable nutzen — ohne Synchronisation können Fehler auftreten! (Mehr dazu in den nächsten Vorlesungen.)
- Auf einen Thread kann man mit Join() warten. Die Methode Join() erlaubt es, den Hauptthread bis zum Abschluss des Hintergrundthreads zu blockieren. Nutze sie, wenn du auf ein Ergebnis warten musst.
- Einen Thread kann man nicht zweimal starten. Nach der Ausführung kann ein Thread nicht erneut gestartet werden — du musst ein neues Thread-Objekt erstellen.
- Beendigung eines Threads. Ein Thread endet, wenn seine Methode fertig ausgeführt ist. Threads gewaltsam zu "killen" ist keine gute Idee (die Methode Abort() gilt seit langem als schädlich und veraltet).
Wozu braucht man Multithreading in der Praxis?
- UI-Anwendungen: die Oberfläche nicht einfrieren lassen, wenn Hintergrund-Downloads oder Berechnungen laufen.
- Server und Dienste: gleichzeitig viele Client-Anfragen bearbeiten.
- Hochleistungsberechnungen: eine große Aufgabe (z.B. Verarbeitung von Millionen von Datensätzen) in Teile zerlegen und parallel ausführen.
- Games, Simulationen, Datenverarbeitung: komplexe Systeme modellieren, ohne Performance zu verlieren.
Probleme des Multithreadings
Multithreading bringt Power, aber auch Komplexität:
- Race Condition (Wettlaufszustand): wenn mehrere Threads gleichzeitig dieselben Daten ändern, hängt das Ergebnis von der Reihenfolge der Anweisungen ab, die unvorhersehbar ist.
- Deadlock (gegenseitige Sperre): Threads warten aufeinander, und niemand kann weitermachen.
- Starvation (Verhungern): ein Thread wird ständig vom Zugriff auf eine Ressource ausgeschlossen.
In dieser Vorlesung haben wir nur die Hauptprobleme von Multithreading erwähnt; in den nächsten lernst du, sie zu erkennen und zu vermeiden. Merke dir vorerst: wenn alles "hängt", können nicht nur Bugs Schuld sein, sondern auch Threads, die sich eine Party veranstaltet haben.
GO TO FULL VERSION