Asynchrone programmeerscenario's

Als u I/O-gebonden behoeften hebt (zoals het aanvragen van gegevens vanuit een netwerk, het openen van een database of het lezen en schrijven naar een bestandssysteem), wilt u asynchrone programmering gebruiken. U kunt ook CPU-gebonden code hebben, zoals het uitvoeren van een dure berekening, wat ook een goed scenario is voor het schrijven van asynchrone code.

C# heeft een asynchroon programmeermodel op taalniveau, waarmee u eenvoudig asynchrone code kunt schrijven zonder dat u callbacks hoeft te gebruiken of aan een bibliotheek moet voldoen die asynchroon ondersteunt. Het volgt wat het op taken gebaseerde Asynchrone patroon (TAP) wordt genoemd.

Overzicht van het asynchrone model

De kern van asynchrone programmering is de Task en Task<T> objecten, die asynchrone bewerkingen modelleren. Ze worden ondersteund door de async trefwoorden en await trefwoorden. Het model is in de meeste gevallen redelijk eenvoudig:

  • Voor I/O-gebonden code wacht u op een bewerking die een Task of Task<T> binnen een async methode retourneert.
  • Voor CPU-gebonden code wacht u op een bewerking die is gestart op een achtergrondthread met de Task.Run methode.

Het await trefwoord is waar de magie plaatsvindt. Het geeft de controle over de aanroeper van de methode die is uitgevoerd awaiten het maakt het uiteindelijk mogelijk dat een gebruikersinterface reageert of dat een service elastisch is. Hoewel er manieren zijn om asynchrone code te benaderen dan async en await, is dit artikel gericht op de constructies op taalniveau.

Notitie

In sommige van de volgende voorbeeldenklasse System.Net.Http.HttpClient wordt gebruikt om bepaalde gegevens van een webservice te downloaden. Het s_httpClient object dat in deze voorbeelden wordt gebruikt, is een statisch veld van Program klasse (controleer het volledige voorbeeld):

private static readonly HttpClient s_httpClient = new();

I/O-gebonden voorbeeld: Gegevens downloaden van een webservice

Mogelijk moet u bepaalde gegevens downloaden uit een webservice wanneer een knop wordt ingedrukt, maar de UI-thread niet wilt blokkeren. Dit kan als volgt worden bereikt:

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);
};

Met de code wordt de intentie (het downloaden van gegevens asynchroon) weergegeven zonder dat er sprake is van interactie met Task objecten.

CPU-gebonden voorbeeld: Een berekening uitvoeren voor een game

Stel dat u een mobiel spel schrijft waarbij het drukken op een knop schade kan toebrengen aan veel vijanden op het scherm. Het uitvoeren van de schadeberekening kan duur zijn en als u dit doet op de UI-thread, lijkt het spel te onderbreken terwijl de berekening wordt uitgevoerd!

De beste manier om dit te doen, is om een achtergrondthread te starten, die het werk doet met behulp Task.Runvan , en het resultaat ervan te wachten met behulp van await. Hierdoor kan de gebruikersinterface zich soepel voelen terwijl het werk wordt uitgevoerd.

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);
};

Met deze code wordt duidelijk de intentie van de klikgebeurtenis van de knop weergegeven. Hiervoor hoeft geen achtergrondthread handmatig te worden beheerd. Dit gebeurt op een niet-blokkerende manier.

Wat gebeurt er onder de dekkingen?

Aan de C#-kant van dingen transformeert de compiler uw code in een statusmachine die dingen bijhoudt, zoals het genereren van uitvoering wanneer een await wordt bereikt en de uitvoering hervat wanneer een achtergrondtaak is voltooid.

Voor de theoretisch geneigde is dit een implementatie van het Promise Model of asynchrony.

Belangrijke onderdelen die u moet begrijpen

  • Asynchrone code kan worden gebruikt voor zowel I/O-gebonden als CPU-gebonden code, maar anders voor elk scenario.
  • Asynchrone code maakt Task<T> gebruik van en Task, die constructies zijn die worden gebruikt om werk op de achtergrond te modelleren.
  • Met async het trefwoord wordt een methode omgezet in een asynchrone methode, waarmee u het trefwoord in de await hoofdtekst kunt gebruiken.
  • Wanneer het await trefwoord wordt toegepast, wordt de aanroepmethode onderbroken en wordt de besturingselement teruggezet naar de aanroeper totdat de wachtende taak is voltooid.
  • await kan alleen worden gebruikt binnen een asynchrone methode.

CPU-gebonden en I/O-gebonden werk herkennen

De eerste twee voorbeelden van deze handleiding hebben laten zien hoe u Iawait/O-gebonden en CPU-gebonden werk kunt gebruikenasync. Het is belangrijk dat u kunt identificeren wanneer een taak die u moet uitvoeren I/O-gebonden of CPU-gebonden is, omdat deze de prestaties van uw code aanzienlijk kan beïnvloeden en mogelijk kan leiden tot verkeerd gebruik van bepaalde constructies.

Hier volgen twee vragen die u moet stellen voordat u code schrijft:

  1. Wacht uw code op iets, zoals gegevens uit een database?

    Als uw antwoord ja is, is uw werk I/O-gebonden.

  2. Voert uw code een dure berekening uit?

    Als u ja hebt beantwoord, is uw werk CPU-gebonden.

Als het werk dat u hebt I/O-gebonden is, gebruikt async u en awaitzonderTask.Run. Gebruik de taakparallelbibliotheek niet .

Als het werk dat u hebt CPU-gebonden is en u wilt reageren, gebruiken async en await, maar het werk op een andere thread afzetten metTask.Run. Als het werk geschikt is voor gelijktijdigheid en parallelle uitvoering, kunt u ook overwegen om de taakparallelbibliotheek te gebruiken.

Daarnaast moet u altijd de uitvoering van uw code meten. U kunt zich bijvoorbeeld in een situatie bevinden waarin uw CPU-gebonden werk niet kostbaar genoeg is vergeleken met de overhead van contextswitches wanneer multithreading wordt uitgevoerd. Elke keuze heeft zijn compromis en u moet de juiste afweging voor uw situatie kiezen.

Meer voorbeelden

In de volgende voorbeelden ziet u verschillende manieren waarop u asynchrone code kunt schrijven in C#. Ze hebben betrekking op een aantal verschillende scenario's die u kunt tegenkomen.

Gegevens extraheren uit een netwerk

Met dit codefragment wordt de HTML uit de opgegeven URL gedownload en wordt het aantal keren geteld dat de tekenreeks .NET voorkomt in de HTML. Er wordt gebruikgemaakt van ASP.NET voor het definiëren van een web-API-controllermethode, waarmee deze taak wordt uitgevoerd en het getal wordt geretourneerd.

Notitie

Als u van plan bent HTML-parsering uit te voeren in productiecode, gebruikt u geen reguliere expressies. Gebruik in plaats daarvan een parseerbibliotheek.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Suspends GetDotNetCount() 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;
}

Dit is hetzelfde scenario dat is geschreven voor een Universele Windows-app, waarmee dezelfde taak wordt uitgevoerd wanneer op een knop wordt gedrukt:

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.
    // This is important to do here, before the "await" call, so that 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 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;
}

Wacht totdat meerdere taken zijn voltooid

Het kan zijn dat u zich in een situatie bevindt waarin u meerdere gegevens tegelijk moet ophalen. De Task API bevat twee methoden Task.WhenAll en Task.WhenAny, waarmee u asynchrone code kunt schrijven waarmee een niet-blokkerende wachttijd op meerdere achtergrondtaken wordt uitgevoerd.

In dit voorbeeld ziet u hoe u gegevens voor een set userIds kunt ophalenUser.

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);
}

Hier volgt een andere manier om dit beknopter te schrijven met behulp van LINQ:

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

Hoewel het minder code is, moet u voorzichtig zijn bij het combineren van LINQ met asynchrone code. Omdat LINQ gebruikmaakt van uitgestelde (luie) uitvoering, worden asynchrone aanroepen niet onmiddellijk uitgevoerd zoals in een foreach lus, tenzij u de gegenereerde reeks dwingt om te herhalen met een aanroep naar .ToList() of .ToArray(). In het bovenstaande voorbeeld wordt Enumerable.ToArray de query gretig uitgevoerd en worden de resultaten opgeslagen in een matrix. Hierdoor wordt de code id => GetUserAsync(id) gedwongen om de taak uit te voeren en te starten.

Belangrijke informatie en advies

Bij asynchroon programmeren zijn er enkele details waarmee u rekening moet houden dat onverwacht gedrag kan voorkomen.

  • asyncmethoden moeten eenawaittrefwoord in hun lichaam hebben of ze zullen nooit opleveren!

    Dit is belangrijk om rekening mee te houden. Als await deze niet wordt gebruikt in de hoofdtekst van een async methode, genereert de C#-compiler een waarschuwing, maar de code compileert en wordt uitgevoerd alsof het een normale methode is. Dit is ongelooflijk inefficiënt, omdat de statusmachine die door de C#-compiler voor de asynchrone methode wordt gegenereerd, niets doet.

  • Voeg 'Async' toe als het achtervoegsel van elke asynchrone methodenaam die u schrijft.

    Dit is de conventie die in .NET wordt gebruikt om synchrone en asynchrone methoden gemakkelijker te onderscheiden. Bepaalde methoden die niet expliciet worden aangeroepen door uw code (zoals gebeurtenis-handlers of methoden voor webcontrollers) zijn niet noodzakelijkerwijs van toepassing. Omdat ze niet expliciet worden aangeroepen door uw code, is het niet zo belangrijk om expliciet te zijn over hun naamgeving.

  • async voidmag alleen worden gebruikt voor gebeurtenis-handlers.

    async void is de enige manier om asynchrone gebeurtenis-handlers te laten werken omdat gebeurtenissen geen retourtypen hebben (dus niet kunnen worden gebruikt en TaskTask<T>). Elk ander gebruik van async void volgt het TAP-model niet en kan lastig zijn om te gebruiken, zoals:

    • Uitzonderingen die in een async void methode worden gegenereerd, kunnen niet buiten die methode worden gevangen.
    • async void methoden zijn moeilijk te testen.
    • async void methoden kunnen slechte bijwerkingen veroorzaken als de aanroeper niet verwacht dat ze asynchroon zijn.
  • Zorgvuldig lezen bij het gebruik van asynchrone lambdas in LINQ-expressies

    Lambda-expressies in LINQ gebruiken de uitgestelde uitvoering, wat betekent dat code uiteindelijk wordt uitgevoerd op een moment dat u dit niet verwacht. De introductie van blokkerende taken kan er eenvoudig toe leiden dat er een impasse ontstaat als deze niet correct is geschreven. Daarnaast kan het nesten van asynchrone code, zoals dit, het lastiger maken om te redeneren over de uitvoering van de code. Async en LINQ zijn krachtig, maar moeten zo zorgvuldig en duidelijk mogelijk samen worden gebruikt.

  • Code schrijven die taken op een niet-blokkerende manier wacht

    Het blokkeren van de huidige thread als een manier om te wachten tot een Task bewerking is voltooid, kan leiden tot impasses en geblokkeerde contextthreads en kan complexere foutafhandeling vereisen. De volgende tabel bevat richtlijnen voor het omgaan met wachten op taken op een niet-blokkerende manier:

    Gebruikt u... In plaats van dit... Als u dit wilt doen...
    await Task.Wait of Task.Result Het resultaat van een achtergrondtaak ophalen
    await Task.WhenAny Task.WaitAny Wachten tot een taak is voltooid
    await Task.WhenAll Task.WaitAll Wachten tot alle taken zijn voltooid
    await Task.Delay Thread.Sleep Wachten op een bepaalde periode
  • Overweeg waar mogelijk gebruik te makenValueTask

    Het retourneren van een Task object vanuit asynchrone methoden kan leiden tot prestatieknelpunten in bepaalde paden. Task is een verwijzingstype, dus als u dit gebruikt, betekent dit dat u een object toewijst. In gevallen waarin een methode die is gedeclareerd met de modifier een resultaat in de async cache retourneert of synchroon wordt voltooid, kunnen de extra toewijzingen een aanzienlijke tijdskosten worden in prestatiekritieke secties van code. Het kan kostbaar worden als deze toewijzingen voorkomen in strakke lussen. Zie gegeneraliseerde asynchrone retourtypen voor meer informatie.

  • Overweeg het gebruik vanConfigureAwait(false)

    Een veelvoorkomende vraag is: 'Wanneer moet ik de Task.ConfigureAwait(Boolean) methode gebruiken?'. Met de methode kan een Task instantie de wachter configureren. Dit is een belangrijke overweging en het onjuist instellen ervan kan mogelijk gevolgen hebben voor de prestaties en zelfs impasses. Zie de veelgestelde vragen over ConfigureAwait voor meer informatieConfigureAwait.

  • Minder stateful code schrijven

    Niet afhankelijk van de status van globale objecten of de uitvoering van bepaalde methoden. In plaats daarvan is dit alleen afhankelijk van de retourwaarden van methoden. Waarom?

    • Code is gemakkelijker te redeneren.
    • Code is eenvoudiger te testen.
    • Het combineren van asynchrone en synchrone code is veel eenvoudiger.
    • Raceomstandigheden kunnen doorgaans helemaal worden vermeden.
    • Afhankelijk van retourwaarden is het coördineren van asynchrone code eenvoudig.
    • (Bonus) het werkt heel goed met afhankelijkheidsinjectie.

Een aanbevolen doel is om volledige of bijna volledige referentiële transparantie in uw code te bereiken. Als u dit doet, resulteert dit in een voorspelbare, testbare en onderhoudbare codebase.

Volledig voorbeeld

De volgende code is de volledige tekst van het Program.cs-bestand voor het voorbeeld.

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()
    {
        // <PerformGameCalculation>
        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);
        };
        // </PerformGameCalculation>
    }

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

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        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);
        };
        // </UnblockingDownload>
    }

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

    // <GetUsersForDataset>
    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);
    }
    // </GetUsersForDataset>

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

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() 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;
    }
    // </ExtractDataFromNetwork>

    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 GetDotNetCount(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("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.

Meer informatie