1. Einleitung
Erinnere dich an unser Beispiel mit dem Laden eines großen Bildes. Während es geladen wird, "hängt" die Anwendung. Dasselbe passiert bei Textdateien, besonders wenn sie groß sind (Logs von Dutzenden Gigabyte, riesige CSV‑Reports, Text‑Backups von Datenbanken).
Stell dir vor, du schreibst eine Anwendung, die:
Eine riesige Logdatei parst, um Fehler zu finden. Wenn du sie synchron liest, friert deine Benutzeroberfläche für einige Sekunden oder sogar Minuten ein, bis der Vorgang abgeschlossen ist. Der Benutzer denkt, das Programm sei abgestürzt.
Während der Generierung Daten in eine Berichtdatei schreibt. Wenn das Schreiben den Hauptthread blockiert, leiden sowohl die Datengenerierung als auch die Arbeit mit der Oberfläche.
Ein Web‑Server, der Tausende Anfragen bedienen muss. Jede Anfrage kann Lesen oder Schreiben von Dateien erfordern. Wenn jeder solche File‑I/O synchron ist, sitzen Server‑Threads untätig und warten auf die Festplatte, und der Server erstickt schnell unter der Last.
In solchen Szenarien wird asynchrones I/O nicht nur zu einer "netten Funktion", sondern zur Lebensnotwendigkeit. Es erlaubt deiner Anwendung, nicht untätig zu bleiben, während die Festplatte "nachdenkt", sondern etwas Nützliches zu tun (z. B. die Oberfläche zu aktualisieren, andere Anfragen zu bearbeiten oder Berechnungen durchzuführen).
Grundbegriffe: async/await und Tasks
- Das Schlüsselwort async zeigt an, dass eine Methode "Wartepunkte" (await) enthalten kann.
- Der Operator await gibt vorübergehend die Kontrolle ab, bis die asynchrone Aufgabe (z. B. das Lesen einer Datei) abgeschlossen ist.
- Eine asynchrone Methode führt I/O aus, ohne den aktuellen Thread zu blockieren: solange keine Daten da sind — ist der Thread frei.
All das ist die Grundlage der asynchronen Magie beim Arbeiten mit Dateien.
2. Asynchrone Methoden für Dateien
In modernen .NET‑Versionen haben fast alle wichtigen Klassen für Dateioperationen asynchrone Gegenstücke. Für Textdateien verwendet man meist:
- StreamReader.ReadLineAsync()
- StreamReader.ReadToEndAsync()
- StreamWriter.WriteLineAsync()
- StreamWriter.WriteAsync()
- Außerdem statische Methoden: File.ReadAllTextAsync(), File.WriteAllTextAsync() usw.
| Lesen | Schreiben |
|---|---|
|
|
|
|
|
|
3. Asynchrones Lesen einer gesamten Textdatei
Lass uns die ganze Datei in einen String lesen. So macht man es bei kleinen Dateien: Konfigs, kleine Logs.
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "input.txt";
// Wir lesen die ganze Datei asynchron
string fileContents = await File.ReadAllTextAsync(path);
Console.WriteLine("Inhalt der Datei:");
Console.WriteLine(fileContents);
}
}
Beachte: die Methode Main ist jetzt mit async Task Main() markiert. Das ist seit C# 7.1 möglich. Ein await — und alles läuft asynchron!
4. Asynchrones zeilenweises Lesen einer großen Datei
Wenn die Datei wirklich groß ist, ist es keine gute Idee, sie komplett in den Speicher zu laden. Besser zeilenweise lesen:
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "biglog.txt";
// Öffnen eines StreamReader für asynchrones Lesen
using StreamReader reader = new StreamReader(path);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
// Hier kann die Zeile verarbeitet werden (z. B. nach Fehlern suchen)
Console.WriteLine(line);
}
}
}
Wie funktioniert das?
Jeder Aufruf von await reader.ReadLineAsync() gibt den Thread frei — besonders nützlich, wenn die Datei auf einem Netzlaufwerk oder in der Cloud liegt. Asynchrone Verarbeitung ist kritisch bei zehntausenden Zeilen und paralleler Benutzerarbeit (z. B. in einer Server‑API).
5. Asynchrones Schreiben von Zeilen in eine Datei
Ähnlich kann man Daten asynchron in eine Datei schreiben (z. B. beim Generieren von Reports):
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "output.txt";
using StreamWriter writer = new StreamWriter(path);
for (int i = 0; i < 5; i++)
{
await writer.WriteLineAsync($"Zeile Nummer {i + 1}");
}
// Man kann explizit FlushAsync aufrufen, um die Schreibvorgänge zu garantieren
await writer.FlushAsync();
Console.WriteLine("Daten asynchron geschrieben!");
}
}
Der Aufruf von FlushAsync() ist nicht immer nötig — beim Schließen des StreamWriter wird der Puffer geleert. Wenn du jedoch sofortige Garantie brauchst, benutze ihn.
6. Zusammenspiel mehrerer asynchroner Dateioperationen
Angenommen, du musst eine Textdatei lesen und parallel eine transformierte Version in eine andere schreiben:
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string sourcePath = "even_biggerlog.txt";
string destinationPath = "copy_biggerlog.txt";
using StreamReader reader = new StreamReader(sourcePath);
using StreamWriter writer = new StreamWriter(destinationPath);
string? line;
int linesProcessed = 0;
while ((line = await reader.ReadLineAsync()) != null)
{
// Ein bisschen Magie: alle Buchstaben in Großbuchstaben umwandeln
string processed = line.ToUpperInvariant();
await writer.WriteLineAsync(processed);
linesProcessed++;
}
Console.WriteLine($"Verarbeitete Zeilen: {linesProcessed}");
}
}
Hier werden Lesen und Schreiben asynchron durchgeführt. Jedes await gibt die Kontrolle frei, sodass die Anwendung etwas anderes tun kann.
7. Praktische Anwendungen: wo wird das verwendet?
- Web‑Entwicklung (ASP.NET Core): Upload/Download von Dateien blockiert nicht die Bearbeitung anderer Anfragen; der Server bleibt responsiv.
- Desktop‑Apps (WPF, WinForms): Beim Öffnen eines Logs oder Speichern eines Reports hängt das UI nicht.
- Game‑Engines: Asynchrones Laden von Ressourcen (Texturen, Modelle) ermöglicht, Animationen und Gameplay nicht zu unterbrechen.
- Big‑Data‑Verarbeitung: Zeilenweises Parsen riesiger CSV/JSON/XML, Streaming‑Verarbeitung ohne unnötigen Speicherverbrauch.
- Hintergrunddienste und Daemons: Logging, Caching, Queue‑Verarbeitung mit effizienter Nutzung von Threads und Festplatte.
Fazit: Asynchronität hilft, moderne, responsive und skalierbare Anwendungen zu bauen. "Blocking" ist schlecht, Asynchronität ist gut!
8. Feinheiten und Best Practices
Vergiss das await nicht! Wenn du eine Methode mit dem Suffix Async ohne Warten aufrufst, erhältst du ein Task, aber der Code geht weiter, was zu Ausführungsreihenfolgefehlern führt.
// SCHLECHT: await vergessen
FileManager.ReadTextFileAsync("nonexistent.txt"); // wird gestartet, aber Main geht weiter
Console.WriteLine("Ich wurde sofort ausgeführt, obwohl die Datei noch gelesen wird (oder schon einen Fehler geworfen hat)! Das ist schlecht!");
Der Compiler wird in der Regel vor einem vergessenen await warnen, aber er stoppt den Build nicht.
using für alles, was IDisposable ist: alle Streams (FileStream, StreamReader, StreamWriter) müssen korrekt freigegeben werden. Verwende using-Blöcke oder using-Deklarationen (C# 8+), um Schließen und Flushen zu garantieren.
Puffergröße (bufferSize): StreamReader/StreamWriter sind bereits optimiert, aber bei speziellen Anforderungen kann man experimentieren. Standardwerte sind in der Regel vernünftig (bei FileStream wird oft 4096 Bytes verwendet).
Fehlerbehandlung: asynchrone Methoden werfen ebenfalls Ausnahmen. Packe Operationen in ein try-catch. Die Ausnahme "steigt hoch", wenn du das entsprechende Task mit await auswertest.
ConfigureAwait: in Bibliotheken und Web‑Szenarios, wo der Synchronisationskontext (GUI) nicht benötigt wird, verwende await SomeAsync().ConfigureAwait(false). Das reduziert den Overhead durch Context‑Switching. In Konsolen‑ und vielen UI‑Apps kann man es oft weglassen.
Übe viel — und bald wird async Task genauso selbstverständlich wie Console.WriteLine.
9. Typische Fehler und wichtige Feinheiten bei asynchronen Dateioperationen
Wenn du kein await benutzt (sondern nur eine Methode mit dem Suffix Async aufrufst), erhältst du ein Task, aber das Ergebnis wird nicht automatisch erwartet. Du musst es mit await abwarten oder explizit blockierend warten (was üblicherweise unerwünscht ist).
Du kannst asynchrone Methoden nicht aus synchronem Code erwarten, ohne das async nach oben durchzureichen. Die Verwendung von .Result oder .GetAwaiter().GetResult() kann zu Deadlocks führen — besser ist es, aufrufende Methoden in async zu verwandeln.
Lese nicht gleichzeitig in dieselbe Datei und schreibe in sie nicht gleichzeitig (auch nicht asynchron). Das kann zu Rennbedingungen und Datenkorruption führen.
Asynchronität befreit den aufrufenden Thread, macht die Operation aber nicht schneller: wenn Festplatte oder Netzwerk langsam sind, bleibt die Operation asynchron ebenso langsam — nur ohne UI‑Blockade oder belegte Worker‑Threads.
GO TO FULL VERSION