EMZETT.
Login

Kurz: Programme warten oft: auf Netzwerk, Datenbank, Datei. Mit async/await warten sie, ohne einen Thread zu blockieren.

Teil des Kurses C#

Kapitel 13 von 16 im Kurs C#. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Programme warten oft: auf Netzwerk, Datenbank, Datei. Mit async/await warten sie, ohne einen Thread zu blockieren: Währenddessen kann das Programm andere Arbeit erledigen (Oberflächen bleiben bedienbar, Server bedienen mehr Anfragen).

Console.WriteLine("start");
string ergebnis = await LadeAsync("Daten");
Console.WriteLine(ergebnis);
 
async Task<string> LadeAsync(string name)
{
    await Task.Delay(100);                                  // simuliert Wartezeit, blockiert keinen Thread
    return $"{name} geladen";
}

Ausgabe:

start
Daten geladen
  • async markiert eine Methode, in der await erlaubt ist
  • Task (ohne Ergebnis) und Task<T> (mit Ergebnis) beschreiben eine laufende Aufgabe
  • await wartet auf das Ergebnis und führt danach den Rest der Methode weiter aus
  • In Konsolen-Programmen mit Top-Level-Anweisungen darfst du direkt await benutzen

Mehrere Aufgaben parallel

var uhr = System.Diagnostics.Stopwatch.StartNew();
 
// nacheinander
string a = await Aufgabe("A", 100);
string b = await Aufgabe("B", 100);
Console.WriteLine($"nacheinander: {a} {b} (>= 200 ms: {uhr.ElapsedMilliseconds >= 190})");
 
// parallel
uhr.Restart();
var t1 = Aufgabe("A", 100);
var t2 = Aufgabe("B", 100);
string[] alle = await Task.WhenAll(t1, t2);
Console.WriteLine($"parallel: {string.Join(" ", alle)} (< 190 ms: {uhr.ElapsedMilliseconds < 190})");
 
string erste = await await Task.WhenAny(Aufgabe("langsam", 200), Aufgabe("schnell", 20));
Console.WriteLine(erste);
 
async Task<string> Aufgabe(string name, int ms) { await Task.Delay(ms); return name; }

Ausgabe:

nacheinander: A B (>= 200 ms: True)
parallel: A B (< 190 ms: True)
schnell

Starte unabhängige Aufgaben zuerst und warte erst dann (Task.WhenAll), statt jede einzeln hintereinander zu erwarten.

Fehler in async-Methoden

try
{
    await Kaputt();
}
catch (InvalidOperationException e)
{
    Console.WriteLine("gefangen: " + e.Message);
}
 
try { await Task.WhenAll(Kaputt(), Task.Delay(10)); }
catch (Exception e) { Console.WriteLine(e.GetType().Name); }
 
async Task Kaputt()
{
    await Task.Delay(10);
    throw new InvalidOperationException("etwas ging schief");
}

Ausgabe:

gefangen: etwas ging schief
InvalidOperationException

Abbruch und Zeitlimit

using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
try
{
    await LangeAufgabe(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("abgebrochen");
}
 
async Task LangeAufgabe(CancellationToken token)
{
    for (int i = 0; i < 100; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(20, token);
    }
}

Ausgabe:

abgebrochen

Gib langen Aufgaben einen CancellationToken mit, damit sie sich abbrechen lassen.

Datenströme: IAsyncEnumerable

await foreach (var zahl in Zahlen())
    Console.Write(zahl + " ");
Console.WriteLine();
 
async IAsyncEnumerable<int> Zahlen()
{
    for (int i = 1; i <= 5; i++)
    {
        await Task.Delay(10);
        yield return i * i;
    }
}

Ausgabe:

1 4 9 16 25

HTTP-Anfragen

Netzwerkzugriffe sind der Hauptanwendungsfall von async:

using var client = new HttpClient();
string json = await client.GetStringAsync("https://example.org/api/daten");
 
var antwort = await client.GetAsync("https://example.org/api/daten");
if (!antwort.IsSuccessStatusCode) throw new Exception($"HTTP {(int) antwort.StatusCode}");
var nutzer = await antwort.Content.ReadFromJsonAsync<Nutzer>();

Erzeuge einen HttpClient einmal und verwende ihn wieder (oder IHttpClientFactory in ASP.NET Core).

Regeln

  1. Async bis ganz oben: async zieht sich durch die Aufrufkette. Vermeide .Result und .Wait() (Deadlock-Gefahr).
  2. Methodennamen enden auf Async (LadeDatenAsync).
  3. async void nur für Event-Handler, sonst async Task.
  4. Task.Run(...) verlagert rechenintensive Arbeit auf einen Pool-Thread. Für Warten (I/O) braucht man das nicht.
  5. In Bibliotheken ConfigureAwait(false) verwenden (nicht in ASP.NET Core Anwendungen und UI-Code nötig).
  6. Mit Locks (lock) darf man nicht awaiten: nimm SemaphoreSlim.

Threads und Parallelität

int summe = 0;
Parallel.For(1, 1001, i => Interlocked.Add(ref summe, i));               // CPU-Arbeit parallel
Console.WriteLine(summe);
 
var ergebnis = Enumerable.Range(1, 10).AsParallel().Select(x => x * x).OrderBy(x => x).ToArray();
Console.WriteLine(string.Join(",", ergebnis));
 
object sperre = new();
int zaehler = 0;
await Task.WhenAll(Enumerable.Range(0, 4).Select(_ => Task.Run(() =>
{
    for (int i = 0; i < 10000; i++) lock (sperre) zaehler++;
})));
Console.WriteLine(zaehler);

Ausgabe:

500500
1,4,9,16,25,36,49,64,81,100
40000

Merke

  • async/await wartet, ohne Threads zu blockieren; Task<T> ist das Versprechen eines Ergebnisses
  • Unabhängiges zuerst starten, dann Task.WhenAll
  • Fehler fängst du mit try/catch rund um await
  • CancellationToken für Abbruch und Zeitlimits
  • Kein .Result/.Wait(); async Task statt async void
  • Parallel.For, Interlocked, lock für CPU-Arbeit und gemeinsame Daten

Übungsaufgabe

Lade (simuliert mit Task.Delay) drei “Webseiten” parallel und gib die Gesamtdauer aus.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Delegates, Lambdas und Events

Weiter: Dateien, JSON und Datum/Zeit

Alle Kapitel: C# im Überblick