1. Einführung
Wenn man sich einen Prozess als Supermarkt vorstellt, sind Threads (thread) die Kassierer an verschiedenen Kassen, die gleichzeitig Kunden bedienen. Alle Kassierer arbeiten im selben Laden, aber jeder erfüllt seine eigene Aufgabe parallel, wodurch die Arbeit schneller und effizienter läuft. Threads innerhalb eines Prozesses ermöglichen es, mehrere Tasks gleichzeitig auszuführen, gemeinsame Ressourcen zu teilen und die Arbeit innerhalb einer Anwendung zu koordinieren. Der Hauptvorteil ist, dass Threads tatsächlich parallel laufen können, wenn die CPU Multitasking unterstützt.
Praktische Notwendigkeit von Threads
Warum sollten wir überhaupt mehrere Threads starten? Hier sind nur ein paar Situationen aus dem echten Leben:
- Du schreibst eine GUI-Anwendung und willst nicht, dass sie während einer langen Operation "einfriert".
- Du musst gleichzeitig mehrere Dateien herunterladen.
- In einem Spiel sollen Gegner unabhängig voneinander denken.
Erstaunlicherweise treten in vielen Programmen immer noch "Hänger" wegen unerfahrenem Umgang mit Threads auf. Heute lernen wir, solche Missgeschicke zu vermeiden.
Die Klasse Thread: Grundlage der manuellen Multithreading-Arbeit
Die Klasse Thread ist ein Dinosaurier der Multithreading-Programmierung in .NET. Trotz modernerer Werkzeuge (Task, async/await) macht es trotzdem Sinn, mit Thread zu arbeiten, besonders wenn man sich die Kontrolle über Threads "von Grund auf" geben möchte.
Schematische Erstellung eines Threads
- Erstelle ein Objekt der Klasse Thread und übergib ihm die Methode, die im Thread ausgeführt werden soll.
- Starte den Thread mit der Methode Start().
- (Optional) Schau nach, was passiert — schließlich kann alles parallel laufen!
2. Einen Thread mit Thread starten
Lass uns die Magie des Parallelismus praktisch erleben. Fügen wir in unsere hypothetische Anwendung eine kleine Klasse hinzu, die bis zu einer bestimmten Zahl zählt und den Fortschritt ausgibt. Wir lernen, wie man diese Arbeit in einem separaten Thread startet.
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("Der Hauptthread hat gestartet!");
// Wir erstellen ein Thread-Objekt und geben die auszuführende Methode an
Thread workerThread = new Thread(CountToTen);
// Wir starten den neuen Thread
workerThread.Start();
// Der Hauptthread macht auch etwas: er schreibt Punkte...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // Verzögerung zur Veranschaulichung
}
Console.WriteLine("\nDer Hauptthread ist fertig!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[Thread] Zählen: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[Thread] Fertig!");
}
}
Was passiert?
Du wirst in der Konsole sehen, dass Punkte und "Zählen: X" durcheinander erscheinen. Das ist das erste Zeichen von Multithreading! Der Hauptthread schreibt seine Punkte, und der neue Thread zählt bis 10. Sie stören sich nicht gegenseitig, wie zwei Musiker in einer Band: einer spielt Schlagzeug, der andere Klavier. Jeder klingt auf seine Weise, und zusammen entsteht Musik.
3. Wie übergibt man Daten an einen Thread?
Manchmal muss ein Thread nicht nur wissen, was zu tun ist, sondern auch, mit was er arbeiten soll. Wenn die Methode für den Thread Parameter erwartet, wie übergibt man diese?
Variante 1: Mit einer Lambda (anonymer Methode)
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[Thread] {i} / {n}");
Thread.Sleep(300);
}
}
Hier wickeln wir den Aufruf der gewünschten Methode in eine Lambda, um Parameter zu übergeben. Das ist sehr verbreitet, denn Thread erwartet eine parameterlose Methode (ThreadStart).
Variante 2: ParameterizedThreadStart verwenden
Man kann den speziellen Delegaten ParameterizedThreadStart verwenden, der einen Parameter vom Typ object akzeptiert.
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[Thread] {i} / {max}");
Thread.Sleep(200);
}
}
Ja, der Parametertyp ist object, daher ist ein Typecast nötig. Nicht ideal, aber es funktioniert! Moderne C#-Varianten bevorzugen jedoch die Lambda-Lösung.
4. Steuerung des Lebenszyklus eines Threads
Lass uns anschauen, welche nützlichen Dinge die Klasse Thread bietet.
| Eigenschaft / Methode | Zweck |
|---|---|
|
Startet den Thread (die bei der Erstellung angegebene Methode) |
|
Wartet auf das Beenden des Threads (blockiert den aufrufenden Thread bis zur Fertigstellung des anderen Threads) |
|
Zeigt an, ob der Thread gerade läuft (true/false) |
|
Ermöglicht, einem Thread einen Namen zu geben (praktisch zum Debuggen) |
|
Gibt das Objekt zurück, das den aktuellen Thread repräsentiert |
|
Stoppt den aktuellen Thread für ms Millisekunden |
Beispiel: Auf einen Thread warten
Manchmal muss der Hauptthread auf das Ende eines Hilfsthreads warten.
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[Zweiter Thread] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[Hauptthread] Wir warten auf das Ende des zweiten Threads...");
t.Join(); // Der Hauptthread wartet hier
Console.WriteLine("[Hauptthread] Der zweite Thread ist beendet!");
Ohne Join() könnte das Programm enden, obwohl der Thread noch arbeitet. Mit Join() wartet der Hauptthread geduldig, bis alle Aufgaben erledigt sind.
5. Nützliche Feinheiten
Thread-Namensgebung: Damit du nicht durcheinanderkommst
Zum Debuggen kann man Threads Namen geben:
Thread t = new Thread(() =>
{
Console.WriteLine($"Das läuft im Thread: {Thread.CurrentThread.Name}");
});
t.Name = "Zähler-Thread";
t.Start();
Das hilft, wenn es viele Threads gibt und jeder seine eigene Aufgabe hat.
Beschränkungen und die reale Zukunft
Kurz gesagt: In modernen Anwendungen ist manuelle Thread-Verwaltung mit Thread eher selten. In der Praxis verwendet man häufiger mächtigere und intelligentere Werkzeuge (Task, async/await), die wir später detailliert behandeln. Aber das Verständnis der Grundlagen von Threads ist wichtig für:
- Verständnis der internen Abläufe von C# und .NET.
- Vorstellungsgespräche (manchmal wird man gebeten, den Unterschied zwischen Thread und Task zu erklären).
- Diagnose und Behebung komplexer Probleme in großen, "vererbten" Anwendungen.
Zusammenfassendes Schema: Lebenszyklus eines Threads
stateDiagram-v2
[*] --> New: Thread erstellt
New --> Running: Start()
Running --> Stopped: Methode beendet
Stopped --> [*]
Jetzt kannst du eigenständig Threads in C# erstellen und starten. Du bist nicht mehr nur ein Passagier im Zug, sondern der Lokführer, der mehrere Waggons steuert! Vor dir liegen Lebenszyklus, Steuerung, Synchronisation und neue Horizonte des Parallelismus.
6. Typische Fehler und Tricks bei der Arbeit mit Thread
Fehler Nr. 1: Den Thread nicht starten.
Oft erstellt man ein Thread-Objekt, vergisst aber, die Methode Start() aufzurufen. Infolgedessen beginnt der Thread nicht zu arbeiten, und die Ursache ist nicht sofort ersichtlich.
Fehler Nr. 2: Gemeinsame Daten ohne Synchronisation ändern.
Wenn mehrere Threads dieselbe Variable ohne Schutz bearbeiten, erwarten dich Probleme! Das ist wie zwei Kassierer, die Wechselgeld aus derselben Kasse herausgeben — schnell entsteht Durcheinander und Fehler.
Fehler Nr. 3: Verwendung veralteter und unsicherer Methoden.
Verwende nicht die Methoden Thread.Suspend(), Thread.Resume() und ähnliche — sie gelten als gefährlich und veraltet. Steuere den Lebenszyklus von Threads auf andere Weise.
Fehler Nr. 4: Unbehandelte Ausnahmen innerhalb von Threads.
Wenn in einem Thread eine Ausnahme auftritt, die nicht gefangen wird, beendet sich der Thread, und der Hauptthread kann davon uninformiert bleiben! Pack den Thread-Code in einen try-catch-Block, um Fehler zu fangen und zu loggen.
Thread t = new Thread(() =>
{
try
{
// ... dein Code
}
catch (Exception ex)
{
// Fehler protokollieren
Console.WriteLine($"[Thread] Fehler: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION