1. Einführung
Ein blockierender Aufruf ist jeder Aufruf, der die Ausführung eines Threads „blockiert“, bis eine Operation abgeschlossen ist. Üblicherweise sind das:
- Lesen oder Schreiben von Daten (z.B. von der Festplatte).
- Lange Datenbank- oder Netzwerkabfragen.
- Aufrufe externer Bibliotheken, die „langsam“ arbeiten.
In solchen Momenten steht der Thread einfach da, scharrt mit den Füßen und wartet: „Na, was ist, wann kommt die Antwort?“
Beispiel aus dem Leben
// Ihre Hauptlogik
Console.WriteLine("Warte auf Antwort vom Server...");
string response = CallServer(); // hier hängt alles!
Console.WriteLine("Server hat geantwortet: " + response);
Solange der Server nicht antwortet, „hängt“ Ihr Thread — und kann nichts anderes tun!
Wie sich das in Anwendungen zeigt
In verschiedenen Anwendungstypen zeigt sich das unterschiedlich. In einem Konsolenprogramm sieht es so aus, als würde es einfach „einfrieren“ — der Cursor hört auf zu blinken und es gibt keine Reaktion. In einer GUI, z.B. WinForms oder WPF, hört das Fenster plötzlich auf zu reagieren, das bekannte „not responding“ erscheint und der Nutzer ist bereit, Ihre Software und Ihren Namen zu verfluchen. Und in Server-Anwendungen ist die Lage noch schlimmer: ein hängender Thread bedeutet einen betreuten Nutzer weniger.
2. Blockierende Aufrufe in C# in der Praxis
Lassen Sie uns die Situation mit den Händen ertasten, indem wir ein Lernprojekt verwenden. Angenommen, wir haben eine Magic-Ball-Anwendung. Dort könnte z.B. folgender Ausschnitt sein:
Console.WriteLine("Gib deine Frage für den magischen Ball ein:");
string question = Console.ReadLine(); // blockierender Aufruf: warten auf Eingabe!
Console.WriteLine("Denke über die Antwort nach...");
Thread.Sleep(5000); // Emulation langer Arbeit
Console.WriteLine("Antwort: versuche es morgen noch einmal!");
- Console.ReadLine() — wartet so lange auf Nutzereingabe, wie es nötig ist (blockiert den Thread).
- Thread.Sleep(5000) — macht künstlich Pause und friert den Thread ein.
Frage: Was ist daran schlimm, wenn wir einfach mit einer Konsolenanwendung arbeiten?
Antwort: In einer Konsolenanwendung ist das nicht so kritisch — Ihr Nutzer wartet ohnehin. Aber was, wenn es mehrere Nutzer gibt? Wenn es ein Server ist, der gleichzeitig 1000 Anfragen bekommt? Oder eine GUI-Anwendung, bei der der Nutzer eine Reaktion der Oberfläche erwartet und nicht Ihre philosophischen Überlegungen?
Beispiel: UI-Blockierung
// In WinForms Button-Handler:
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "Lade...";
DoHeavyWork(); // schwere Operation (z.B. DB-Abfrage)
label1.Text = "Fertig!";
}
Problem: Solange DoHeavyWork läuft, aktualisiert das Fenster die Oberfläche nicht, reagiert nicht auf Tastatur und Maus, wird nicht neu gezeichnet. Wenn der Nutzer versucht, das Fenster zu schließen — zeigt Windows „respondet nicht“ und bietet an, den Task zu beenden.
3. Die große Schmerzstelle der Multithread-Welt
Probleme blockierender Aufrufe
- Ressourcenverschwendung. Jeder Thread (Thread) in .NET ist ein ziemlich „schweres“ Objekt. Das OS reserviert Stack-Speicher (normalerweise 1 MB), Handles, Synchronisation usw. Wenn ein Thread „inaktiv“ ist (wartet auf Datei oder Netzwerk), macht er sinnlose Arbeit („nichts tun, aber verbraucht Speicher“).
- Begrenztheit der Threads. In Server-Anwendungen gibt es üblicherweise einen Pool von Threads. Wenn alle Threads blockieren — werden neue Anfragen nicht bearbeitet. Beispiel ASP.NET: wenn alle verfügbaren Threads auf die Datenbank warten, „hängt“ die Seite für alle neuen Nutzer.
- Schlechte UI-Reaktionszeit. In GUI-Anwendungen reagiert das Fenster nicht mehr, wenn der UI-Thread blockiert ist.
- Leistungseinbruch. Je mehr Threads blockiert sind, desto mehr Kontextwechsel, Belastung des OS, und desto langsamer wird der ganze Rechner.
Typische Quellen blockierender Aufrufe
Schauen wir, welche Aufrufe einen Thread „einfach so“ blockieren können:
- Netzwerkoperationen: Aufrufe an APIs, Herunterladen von Dateien, Updates downloaden.
- Festplatte/Dateisystem: Lesen oder Schreiben großer Dateien.
- Datenbanken: lange SQL-Abfragen oder selbst lokale DB-Abfragen.
- Nutzereingabe: langes ReadLine — klein, aber blockierend.
- Thread.Sleep, Task.Delay: frieren den Thread künstlich ein.
Beispiel (Daten von einer Website laden)
using System.Net.Http;
HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // blockierender synchroner Aufruf!
Console.WriteLine(result);
Was passiert hier? Die Methode .Result blockiert den Thread, bis eine Antwort kommt!
4. Wie merkt man, dass Ihr Aufruf den Thread blockiert?
Ganz einfach: Wenn eine Methode die Kontrolle nicht zurückgibt, bis ihre Arbeit beendet ist (Warten, Eingabe, Netzwerk, Festplatte) — dann ist es ein blockierender Aufruf.
- Methoden mit Thread.Sleep, Task.Wait, .Result, .Wait() — blockieren.
- Methoden, die Lesen/Schreiben ohne Asynchronität durchführen — ebenfalls.
- Ein „hängendes“ Fenster oder ein eingefrorener Server — ein häufiges Zeichen.
Visuelles Element: Flussdiagramm eines blockierenden Aufrufs
+-------------------+
| Methodenstart |
+-------------------+
|
v
+-------------------+
| Aufruf blockierender|
| Methode (z.B. |
| Datei lesen) |
+-------------------+
|
v
+-------------------+
| Warten auf Abschluss|
| der Operation |
+-------------------+
|
v
+-------------------+
| Rückkehr aus der |
| Methode und weiter |
+-------------------+
5. Warum blockierende Aufrufe in Server-Anwendungen schlecht sind
Die meisten modernen Anwendungen sind netzwerk- oder serverbasiert. Selbst wenn Sie ein Spiel schreiben, gibt es oft Ladevorgänge vom Server. Wenn Sie eine Website bauen, gibt es definitiv Netzwerk- und Dateizugriffe.
Stellen Sie sich vor, Ihr Server verarbeitet 1000 Requests pro Sekunde. Jeder Request muss mit der DB reden. Sie schreiben:
// Web-Handler
string data = db.ReadDataSync(); // synchroner Block!
return new Response(data);
Solange die DB-Abfrage läuft, sitzt der Thread gelangweilt da und wartet. Ein Request — na gut. Aber wenn sich mehrere ansammeln, sind alle Threads voll. Neue Requests passen nicht mehr rein und müssen warten. Am Ende wird der Server eher zu einer langen Warteschlange als zu einer Maschine, die arbeitet.
Illustration: "Synchrone Anfrageverarbeitung"
Request 1: belegt Thread -> wartet auf DB -> frei
Request 2: belegt Thread -> wartet auf DB -> frei
...
Request 100: keine freien Threads, wartet in der Warteschlange...
6. Asynchronität — das Gegenmittel gegen blockierende Aufrufe
Um Blockieren zu vermeiden, verwendet man asynchrone Aufrufe. In C# sind das die magischen Wörter: async und await.
Sie ermöglichen:
- Den Thread nicht zu blockieren (besonders wertvoll für UI- oder Server-Threads).
- Ressourcen nicht sinnlos zu belegen.
- Die Reaktionsfähigkeit der GUI zu verbessern.
- Den Thread nicht festzuhalten, während eine langsame Operation läuft.
Beachte: man fängt nicht sofort mit Asynchronität an, weil sie Verständnis für Threads und Blockaden erfordert. Jetzt, wo Du die Qualen blockierender Aufrufe kennst, wird der asynchrone Ansatz wie ein echter Nervenkitzel für Gehirn und Nutzer erscheinen.
Tabelle: Was blockiert und was nicht?
| Methode/Aufruf | Blockiert den Thread | Geeignet für UI/Server |
|---|---|---|
|
Ja | Nein |
|
Ja | Nein |
|
Ja | Nein |
|
Ja | Nein |
|
Ja | Nein |
|
Nein | Ja |
|
Nein | Ja |
|
Nein | Ja |
NB: await funktioniert nur in einer asynchronen Funktion (async Task ...), die wir in den nächsten Vorlesungen genauer behandeln.
7. Legendäre Fehler beim Umgang mit blockierenden Aufrufen
„Blockiere die UI — der Nutzer verflucht alles“
private void btnLoad_Click(object sender, EventArgs e)
{
var data = BigFileReader.Read(@"C:\huge.dat"); // blockierender Aufruf!
textBox1.Text = data;
}
Solange die riesige Datei nicht gelesen ist — hängt das Fenster.
„Server hängt — die Clients rennen weg“
public IActionResult Download()
{
var content = File.ReadAllBytes("bigfile.zip"); // synchron, lange, blockiert den ASP.NET-Thread!
return File(content, "application/zip");
}
Kleine Server (z.B. auf kostenlosem Hosting) können bei so einem Ansatz einfach „abstürzen“.
„Async-Methode + .Wait()/.Result = synchroner Tod“
public void LoadData()
{
var result = DoAsyncWork().Result; // Blockiert den Thread!
}
.Result und .Wait() verwandeln selbst eleganten asynchronen Code in einen banalen blockierenden Aufruf.
GO TO FULL VERSION