Freigeben über


Asynchrone Programmierszenarios

Wenn Ihr Code E/A-gebundene Szenarien implementiert, um Netzwerkdatenanforderungen, Datenbankzugriff oder Dateisystemlese-/Schreibvorgänge zu unterstützen, ist die asynchrone Programmierung der beste Ansatz. Sie können auch asynchronen Code für CPU-gebundene Szenarien wie teure Berechnungen schreiben.

C# verfügt über ein asynchrones Programmiermodell auf Sprachebene, mit dem Sie auf einfache Weise asynchronen Code schreiben können, ohne mit Rückrufen hantieren zu müssen oder eine Bibliothek verwenden zu müssen, die Asynchronität unterstützt. Das Modell folgt dem sogenannten Aufgabenbasierten asynchronen Muster (TAP).The model follows what is known as the Task-based asynchron pattern (TAP).

Erkunden des asynchronen Programmiermodells

Die Objekte Task und Task<T> repräsentieren den Kern der asynchronen Programmierung. Diese Objekte werden verwendet, um asynchrone Vorgänge zu modellieren, indem sie die async Schlüsselwörter await unterstützen. In den meisten Fällen ist das Modell für I/O-gebundene und CPU-gebundene Szenarien relativ einfach. Innerhalb einer async Methode:

  • I/O-gebundener Code startet einen Vorgang, der durch ein Task Oder Task<T> Objekt innerhalb der async Methode dargestellt wird.
  • CPU-gebundener Code startet einen Vorgang in einem Hintergrundthread mit der Task.Run Methode.

In beiden Fällen stellt ein aktiver Task Vorgang einen asynchronen Vorgang dar, der möglicherweise nicht abgeschlossen ist.

Das Schlüsselwort await ist sozusagen der Zauberstab. Sie liefert die Steuerung für den Aufrufer der Methode, die den await Ausdruck enthält, und ermöglicht letztendlich, dass die Benutzeroberfläche reaktionsfähig ist oder ein Dienst elastisch ist. Obwohl es Möglichkeiten gibt, sich asynchronem Code anders als durch die Verwendung der async und await Ausdrücke zu nähern, konzentriert sich dieser Artikel auf die Konstrukte auf Sprachebene.

Hinweis

Einige Beispiele in diesem Artikel verwenden die System.Net.Http.HttpClient Klasse zum Herunterladen von Daten aus einem Webdienst. Im Beispielcode ist das s_httpClient Objekt ein statisches Feld der Typklasse Program :

private static readonly HttpClient s_httpClient = new();

Weitere Informationen finden Sie im vollständigen Beispielcode am Ende dieses Artikels.

Überprüfen der zugrunde liegenden Konzepte

Wenn Sie die asynchrone Programmierung in Ihrem C#-Code implementieren, wandelt der Compiler Ihr Programm in einen Zustandsautomaten um. Dieses Konstrukt verfolgt verschiedene Operationen und Zustände in Ihrem Code, wie z. B. das Nachgeben der Ausführung, wenn der Code einen await-Ausdruck erreicht, und die Wiederaufnahme der Ausführung, wenn ein Job im Hintergrund abgeschlossen ist.

In Bezug auf die Informatiktheorie ist die asynchrone Programmierung eine Implementierung des Zusagemodells von Asynchronie.

Im asynchronen Programmiermodell gibt es mehrere wichtige Konzepte, die Sie verstehen sollten:

  • Sie können asynchronen Code sowohl für I/O-gebundenen als auch für CPU-gebundenen Code verwenden, die Implementierung ist jedoch unterschiedlich.
  • Asynchroner Code verwendet Task<T> und Task Objekte als Konstrukte, um die im Hintergrund laufende Arbeit zu modellieren.
  • Das async Schlüsselwort deklariert eine Methode als asynchrone Methode, mit der Sie das await Schlüsselwort im Methodentext verwenden können.
  • Wenn Sie das await Schlüsselwort anwenden, hält der Code die aufrufende Methode an und gibt die Kontrolle zurück an den Aufrufer, bis die Aufgabe abgeschlossen ist.
  • Sie können den await Ausdruck nur in einer asynchronen Methode verwenden.

E/A-gebundenes Beispiel: Herunterladen von Daten aus dem Webdienst

Wenn der Benutzer in diesem Beispiel eine Schaltfläche auswählt, lädt die App Daten aus einem Webdienst herunter. Sie möchten den UI-Thread für die App während des Downloadvorgangs nicht blockieren. Der folgende Code führt diese Aufgabe aus:

s_downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Der Code gibt die Absicht (Daten asynchron herunterladen) an, ohne durch Interaktion mit Task-Objekten vereitelt zu werden.

CPU-gebundenes Beispiel: Ausführen der Spielberechnung

Im nächsten Beispiel verursacht ein mobiles Spiel als Reaktion auf ein Schaltflächenereignis Schäden an mehreren Agents auf dem Bildschirm. Das Ausführen der Schadensberechnung kann teuer sein. Das Ausführen der Berechnung im UI-Thread kann während der Berechnung zu Anzeige- und UI-Interaktionsproblemen führen.

Die beste Möglichkeit zum Behandeln der Aufgabe besteht darin, einen Hintergrundthread zu starten, um die Arbeit mit der Task.Run Methode abzuschließen. Die Operation ergibt sich durch die Verwendung eines await-Ausdrucks. Der Vorgang wird fortgesetzt, wenn die Aufgabe abgeschlossen ist. Mit diesem Ansatz kann die Benutzeroberfläche reibungslos ausgeführt werden, während die Arbeit im Hintergrund abgeschlossen ist.

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code omitted:
        //
        // Does an expensive calculation and returns
        // the result of that calculation.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Der Code drückt eindeutig die Absicht des Schaltflächenereignisses Clicked aus. Es ist nicht erforderlich, einen Hintergrundthread manuell zu verwalten, und die Aufgabe wird auf nicht blockierende Weise erledigt.

Erkennen Sie CPU-gebundene und E/A-gebundene Szenarien

In den vorherigen Beispielen wird die Verwendung des async Modifizierers und des await Ausdrucks für I/O-gebundene und CPU-gebundene Arbeiten veranschaulicht. Ein Beispiel für jedes Szenario zeigt, wie sich der Code abhängig davon unterscheidet, wo die Operation gebunden ist. Um sich auf Ihre Implementierung vorzubereiten, müssen Sie verstehen, wie Sie ermitteln können, wann ein Vorgang I/O-gebunden oder CPU-gebunden ist. Ihre Implementierungsauswahl kann sich erheblich auf die Leistung Ihres Codes auswirken und möglicherweise zu fehlverwendenden Konstrukten führen.

Es gibt zwei hauptfragen, die Sie behandeln müssen, bevor Sie Code schreiben:

Frage Szenario Implementierung
Sollte der Code auf ein Ergebnis oder eine Aktion warten, z. B. Daten aus einer Datenbank? E/A-gebunden Verwenden Sie den async-Modifikator und den await-Ausdruck ohne die Task.Run-Methode.

Vermeiden Sie die Verwendung der parallelen Aufgabenbibliothek.
Sollte der Code eine teure Berechnung ausführen? CPU-gebunden Verwenden Sie den async-Modifikator und den await-Ausdruck, aber verlagern Sie die Arbeit mit der Task.Run-Methode auf einen anderen Thread. Bei diesem Ansatz geht es um Probleme mit der CPU-Reaktionsfähigkeit.

Wenn die Arbeit für Parallelität und Konkurrenz geeignet ist, erwägen Sie auch die Verwendung der Task Parallel Library.

Messen Sie immer die Ausführung des Codes. Möglicherweise stellen Sie fest, dass Ihre CPU-gebundene Arbeit nicht kostspielig genug ist, verglichen mit dem Aufwand von Kontextschaltern beim Multithreading. Jede Wahl hat Kompromisse. Wählen Sie den richtigen Kompromiss für Ihre Situation aus.

Weitere Beispiele entdecken

Die Beispiele in diesem Abschnitt veranschaulichen verschiedene Möglichkeiten, asynchronen Code in C# zu schreiben. Sie decken einige Szenarien ab, denen Sie möglicherweise begegnen könnten.

Extrahieren von Daten aus einem Netzwerk

Der folgende Code lädt HTML aus einer bestimmten URL herunter und zählt, wie oft die Zeichenfolge ".NET" im HTML-Code auftritt. Der Code verwendet ASP.NET zum Definieren einer Web-API-Controllermethode, die die Aufgabe ausführt und die Anzahl zurückgibt.

Hinweis

Wenn Sie eine HTML-Analyse im Produktionscode durchführen möchten, nutzen Sie dafür nicht die regulären Ausdrücke. Verwenden Sie stattdessen eine Analysebibliothek.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCountAsync(string URL)
{
    // Suspends GetDotNetCountAsync() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Sie können ähnlichen Code für eine universelle Windows-App schreiben und die Zählaufgabe ausführen, nachdem eine Schaltfläche gedrückt wurde:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // It's important to do the extra work here before the "await" call,
    // so the user sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
    // This action is what allows the app to be responsive and not block the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Warten auf das Abschließen mehrerer Tasks

In einigen Szenarien muss der Code mehrere Datenabschnitte gleichzeitig abrufen. Die Task APIs stellen Methoden bereit, mit denen Sie asynchronen Code schreiben können, der eine nicht blockierte Wartezeit für mehrere Hintergrundaufträge ausführt:

Das folgende Beispiel zeigt, wie Sie Daten eines User Objekts für eine Gruppe von userId Objekten abrufen können.

private static async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Sie können diesen Code prägnant schreiben, indem Sie LINQ verwenden:

private static async Task<User[]> GetUsersByLINQAsync(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Obwohl Sie weniger Code mit LINQ schreiben, sollten Sie Vorsicht walten lassen beim Einsatz von LINQ zusammen mit asynchronem Code. LINQ verwendet verzögerte (oder faule) Ausführung, was bedeutet, dass ohne sofortige Auswertung asynchrone Aufrufe erst ausgeführt werden, wenn die Sequenz aufgezählt wird.

Das vorherige Beispiel ist korrekt und sicher, da die Enumerable.ToArray Methode verwendet wird, um die LINQ-Abfrage sofort auszuwerten und die Aufgaben in einem Array zu speichern. Durch diesen Ansatz wird sichergestellt, dass die id => GetUserAsync(id) Aufrufe sofort ausgeführt werden und alle Aufgaben gleichzeitig gestartet werden, genau wie der foreach Loop-Ansatz. Verwenden Enumerable.ToArray Sie immer oder Enumerable.ToList erstellen Sie Aufgaben mit LINQ, um eine sofortige Ausführung und gleichzeitige Aufgabenausführung sicherzustellen. Im Folgenden finden Sie ein Beispiel, das veranschaulicht, wie ToList() zusammen mit Task.WhenAny genutzt wird, um Aufgaben bei deren Abschluss zu verarbeiten.

private static async Task ProcessTasksAsTheyCompleteAsync(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToList();
    
    while (getUserTasks.Count > 0)
    {
        Task<User> completedTask = await Task.WhenAny(getUserTasks);
        getUserTasks.Remove(completedTask);
        
        User user = await completedTask;
        Console.WriteLine($"Processed user {user.id}");
    }
}

In diesem Beispiel wird eine Liste erstellt, ToList() die den Remove() Vorgang unterstützt, sodass Sie erledigte Aufgaben dynamisch entfernen können. Dieses Muster ist besonders nützlich, wenn Sie Ergebnisse behandeln möchten, sobald sie verfügbar sind, anstatt darauf zu warten, dass alle Aufgaben abgeschlossen sind.

Obwohl Sie weniger Code mit LINQ schreiben, sollten Sie Vorsicht walten lassen beim Einsatz von LINQ zusammen mit asynchronem Code. LINQ verwendet verzögerte (oder faule) Ausführung. Asynchrone Aufrufe erfolgen nicht sofort, wie in einer foreach Schleife, es sei denn, Sie zwingen die generierte Sequenz dazu, mit einem Aufruf der .ToList() oder .ToArray() Methode durchlaufen zu werden.

Sie können zwischen Enumerable.ToArray und Enumerable.ToList basierend auf Ihrem Szenario wählen:

  • Verwenden Sie ToArray(), wenn Sie beabsichtigen, alle Aufgaben zusammen zu verarbeiten, z. B. mit Task.WhenAll. Arrays sind effizient für Szenarien, in denen die Sammlungsgröße festgelegt ist.
  • Verwenden Sie ToList(), wenn Sie Aufgaben dynamisch verwalten müssen, z. B. mit Task.WhenAny, wo Sie erledigte Aufgaben nach Abschluss aus der Sammlung entfernen können.

Überlegungen zur asynchronen Programmierung

Bei der asynchronen Programmierung sind mehrere Details zu beachten, die unerwartetes Verhalten verhindern können.

Verwenden Sie await im Body der async()-Methode

Wenn Sie den async Modifizierer verwenden, sollten Sie einen oder mehrere await Ausdrücke in den Methodenkörper einschließen. Wenn der Compiler keinen Ausdruck await findet, liefert die Methode keinen Wert. Obwohl der Compiler eine Warnung generiert, wird der Code weiterhin kompiliert, und der Compiler führt die Methode aus. Die Zustandsmaschine, die vom C#-Compiler für die asynchrone Methode generiert wird, führt zu keinem Ergebnis, sodass der gesamte Prozess sehr ineffizient ist.

Hinzufügen von "Async"-Suffix zu asynchronen Methodennamen

Die .NET-Formatkonvention besteht darin, das Suffix "Async" allen asynchronen Methodennamen hinzuzufügen. Dieser Ansatz hilft dabei, synchrone und asynchrone Methoden einfacher zu unterscheiden. Bestimmte Methoden, die nicht explizit von Ihrem Code aufgerufen werden (z. B. Ereignishandler oder Webcontrollermethoden), gelten in diesem Szenario nicht unbedingt. Da diese Elemente nicht explizit von Ihrem Code aufgerufen werden, ist die Verwendung expliziter Benennungen nicht so wichtig.

Geben Sie 'async void' nur von Event Handlern zurück

Ereignishandler müssen void Rückgabetypen deklarieren und können Task- und Task<T>-Objekte weder verwenden noch zurückgeben, wie es andere Methoden tun. Wenn Sie asynchrone Event Handler schreiben, müssen Sie den async-Modifikator für eine void-Rückgabemethode für die Handler verwenden. Andere Implementierungen von Methoden, die async void zurückgeben, folgen nicht dem TAP-Modell und können Herausforderungen mit sich bringen.

  • Ausnahmen, die in einer async void Methode ausgelöst werden, können nicht außerhalb dieser Methode abgefangen werden.
  • async void Methoden sind schwierig zu testen
  • async void Methoden können negative Nebenwirkungen verursachen, wenn der Aufrufer nicht erwartet, dass sie asynchron sind.

Verwenden Sie Vorsicht bei asynchronen Lambdas in LINQ

Es ist wichtig, Vorsicht zu verwenden, wenn Sie asynchrone Lambdas in LINQ-Ausdrücken implementieren. Lambda-Ausdrücke in LINQ verwenden verzögerte Ausführung, was bedeutet, dass der Code zu einem unerwarteten Zeitpunkt ausgeführt werden kann. Die Einführung von Blockierungsaufgaben in dieses Szenario kann leicht zu einem Deadlock führen, wenn der Code nicht ordnungsgemäß geschrieben wurde. Außerdem kann es durch die Verschachtelung von asynchronem Code schwierig werden, die Ausführung des Codes zu begründen. Async und LINQ sind leistungsfähig, aber diese Techniken sollten so sorgfältig und deutlich wie möglich verwendet werden.

Yield für Aufgaben in einer nicht blockierenden Weise

Wenn Ihr Programm das Ergebnis einer Aufgabe benötigt, schreiben Sie Code, der den await Ausdruck auf nicht blockierte Weise implementiert. Das Blockieren des aktuellen Threads als Mittel, synchron zu warten, bis ein Task Element abgeschlossen ist, kann zu Deadlocks und blockierten Kontextthreads führen. Dieser Programmieransatz kann eine komplexere Fehlerbehandlung erfordern. In der folgenden Tabelle finden Sie Anleitungen dazu, wie man auf Ergebnisse von Aufgaben auf nicht-blockierende Weise zugreifen kann.

Aufgabenszenario Aktueller Code Ersetzen durch "await"
Abrufen des Ergebnisses einer Hintergrundaufgabe Task.Wait oder Task.Result await
Fortfahren, wenn eine Aufgabe abgeschlossen ist Task.WaitAny await Task.WhenAny
Fortfahren, wenn alle Aufgaben abgeschlossen sind Task.WaitAll await Task.WhenAll
Nach einiger Zeit fortfahren Thread.Sleep await Task.Delay

Erwägen Sie die Verwendung des ValueTask-Typs.

Wenn eine asynchrone Methode ein Task Objekt zurückgibt, können Leistungsengpässe in bestimmten Pfaden eingeführt werden. Da Task ein Referenztyp ist, wird ein Task-Objekt aus dem Heap zugewiesen. In Fällen, in denen eine mit dem async-Modifizierer deklarierte Methode ein zwischengespeichertes Ergebnis zurückgibt oder synchron abschließt, können die zusätzlichen Zuordnungen viel Zeit bei der Ausführung kritischer Codeabschnitte kosten. Dieses Szenario kann kostspielig werden, wenn die Zuordnungen in engen Schleifen ausgeführt werden. Weitere Informationen finden Sie unter Generalisierte asynchrone Rückgabetypen.

Verstehen, wann ConfigureAwait(false) festgelegt werden soll

Entwickler fragen häufig, wann der Task.ConfigureAwait(Boolean) boolesche Wert verwendet werden soll. Diese API ermöglicht es einer Task Instanz, den Kontext für den Zustandscomputer zu konfigurieren, der einen beliebigen await Ausdruck implementiert. Wenn der Boolean nicht ordnungsgemäß festgelegt ist, kann die Leistung beeinträchtigt werden oder Deadlocks können auftreten. Weitere Informationen finden Sie unter ConfigureAwait FAQ.

Schreiben Sie weniger statusbasierten Code

Vermeiden Sie das Schreiben von Code, der vom Status globaler Objekte oder der Ausführung bestimmter Methoden abhängt. Seien Sie stattdessen nur abhängig von Rückgabewerten der Methoden. Es hat viele Vorteile, Code zu schreiben, der weniger statusbasiert ist:

  • Es ist einfacher, über den Code nachzudenken
  • Einfacheres Testen von Code
  • Einfacheres Kombinieren von asynchronen und synchronen Code
  • Sie können Race Conditions im Code vermeiden.
  • Einfaches Koordinieren von asynchronen Code, der von Rückgabewerten abhängt
  • (Bonus) Funktioniert gut mit Dependency Injection im Code

Ein empfohlenes Ziel ist das vollständige oder nahezu vollständige Erreichen referenzieller Transparenz in Ihrem Code. Dieser Ansatz führt zu einer vorhersehbaren, testbaren und wartungsfähigen Codebasis.

Synchroner Zugriff auf asynchrone Vorgänge

In Szenarien müssen Sie möglicherweise asynchrone Vorgänge blockieren, wenn das await Schlüsselwort während des gesamten Aufrufstapels nicht verfügbar ist. Diese Situation tritt in älteren Codebasen oder bei der Integration asynchroner Methoden in synchrone APIs auf, die nicht geändert werden können.

Warnung

Synchrone Blockierung bei asynchronen Vorgängen kann zu Deadlocks führen und sollte möglichst vermieden werden. Die bevorzugte Lösung besteht darin, async/await durchgehend im gesamten Aufrufstapel zu verwenden.

Wenn Sie synchron auf einem Task blockieren müssen, sind die folgenden Ansätze verfügbar, aufgeführt von den beliebtesten bis zu den am wenigsten bevorzugten:

Verwenden Sie GetAwaiter(). GetResult()

Das GetAwaiter().GetResult() Muster ist im Allgemeinen der bevorzugte Ansatz, wenn Sie synchron blockieren müssen:

// When you cannot use await
Task<string> task = GetDataAsync();
string result = task.GetAwaiter().GetResult();

Dieser Ansatz:

  • Behält die ursprüngliche Ausnahme bei, ohne sie in eine AggregateException zu umschließen.
  • Blockiert den aktuellen Thread, bis die Aufgabe abgeschlossen ist.
  • Birgt immer noch Deadlock-Risiko, wenn sie nicht sorgfältig verwendet wird.

Verwenden von Task.Run für komplexe Szenarien

Für komplexe Szenarien, in denen Sie die asynchrone Arbeit isolieren müssen:

// Offload to thread pool to avoid context deadlocks
string result = Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();

Dieses Muster:

  • Führt die asynchrone Methode auf einem Threadpool-Thread aus.
  • Kann dazu beitragen, einige Deadlock-Szenarien zu vermeiden.
  • Fügt Overhead hinzu, indem Arbeit zum Threadpool geplant wird.

Verwenden von Wait() und Result

Sie können einen Blockierungsansatz verwenden, indem Sie aufrufen Wait() und Result. Dieser Ansatz wird jedoch abgeraten, da er Ausnahmen umschließt in AggregateException.

Task<string> task = GetDataAsync();
task.Wait();
string result = task.Result;

Probleme mit Wait() und Result:

  • Ausnahmen werden von AggregateException umschlossen, wodurch die Fehlerbehandlung komplexer wird.
  • Höheres Deadlock-Risiko.
  • Weniger klare Absicht im Code.

Weitere Überlegungen

  • Deadlock-Verhinderung: Achten Sie besonders auf Benutzeroberflächenanwendungen oder bei Verwendung eines Synchronisierungskontexts.
  • Leistungseinbußen: Das Blockieren von Threads reduziert die Skalierbarkeit.
  • Ausnahmebehandlung: Testen Sie Fehlerszenarien sorgfältig, da sich das Ausnahmeverhalten zwischen Mustern unterscheidet.

Ausführlichere Anleitungen zu den Herausforderungen und Überlegungen synchroner Wrapper für asynchrone Methoden finden Sie unter "Sollte ich synchrone Wrapper für asynchrone Methoden verfügbar machen?".

Überprüfen des vollständigen Beispiels

Der folgende Code stellt das vollständige Beispiel dar, das in der Program.cs Beispieldatei verfügbar ist.

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/maui"
    };

    private static void Calculate()
    {
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_calculateButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI while CalculateDamageDone()
            // performs its work. The UI thread is free to perform other work.
            var damageResult = await Task.Run(() => CalculateDamageDone());
            DisplayDamage(damageResult);
        };
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        s_downloadButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI as the request
            // from the web service is happening.
            //
            // The UI thread is now free to perform other work.
            var stringData = await s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine($"Displaying data: {stringData}");
    }

    private static async Task<User> GetUserAsync(int userId)
    {
        // Code omitted:
        //
        // Given a user Id {userId}, retrieves a User object corresponding
        // to the entry in the database with {userId} as its Id.

        return await Task.FromResult(new User() { id = userId });
    }

    private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = new List<Task<User>>();
        foreach (int userId in userIds)
        {
            getUserTasks.Add(GetUserAsync(userId));
        }

        return await Task.WhenAll(getUserTasks);
    }

    private static async Task<User[]> GetUsersByLINQAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }

    private static async Task ProcessTasksAsTheyCompleteAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToList();
        
        while (getUserTasks.Count > 0)
        {
            Task<User> completedTask = await Task.WhenAny(getUserTasks);
            getUserTasks.Remove(completedTask);
            
            User user = await completedTask;
            Console.WriteLine($"Processed user {user.id}");
        }
    }

    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCountAsync(string URL)
    {
        // Suspends GetDotNetCountAsync() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCountAsync(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Processing tasks as they complete...");
        await ProcessTasksAsTheyCompleteAsync(ids);

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.