Asynkrona programmeringsscenarier

Om du har några I/O-bundna behov (till exempel att begära data från ett nätverk, komma åt en databas eller läsa och skriva till ett filsystem) vill du använda asynkron programmering. Du kan också ha CPU-bunden kod, till exempel att utföra en dyr beräkning, vilket också är ett bra scenario för att skriva asynkron kod.

C# har en asynkron programmeringsmodell på språknivå som gör det enkelt att skriva asynkron kod utan att behöva jonglera motringningar eller följa ett bibliotek som stöder asynkron kod. Den följer det så kallade aktivitetsbaserade asynkrona mönstret (TAP).

Översikt över den asynkrona modellen

Kärnan i asynkron programmering är objekten Task och Task<T> som modellerar asynkrona åtgärder. De stöds av nyckelorden async och await . Modellen är ganska enkel i de flesta fall:

  • För I/O-bunden kod väntar du på en åtgärd som returnerar en Task eller Task<T> inuti en async metod.
  • För CPU-bunden kod väntar du på en åtgärd som startas i en bakgrundstråd med Task.Run metoden .

Nyckelordet await är där magin händer. Den ger kontroll till anroparen för den metod som utförde await, och i slutändan gör det möjligt för ett användargränssnitt att vara dynamiskt eller att en tjänst är elastisk. Även om det finns sätt att använda asynkron kod förutom async och awaitfokuserar den här artikeln på konstruktionerna på språknivå.

Kommentar

I några av följande exempel System.Net.Http.HttpClient används klassen för att ladda ned vissa data från en webbtjänst. Objektet s_httpClient som används i dessa exempel är ett statiskt fält i Program klassen (kontrollera hela exemplet):

private static readonly HttpClient s_httpClient = new();

I/O-bundet exempel: Ladda ned data från en webbtjänst

Du kan behöva ladda ned vissa data från en webbtjänst när en knapp trycks ned men inte vill blockera användargränssnittstråden. Det kan åstadkommas så här:

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

Koden uttrycker avsikten (laddar ned data asynkront) utan att fastna i interaktionen med Task objekt.

CPU-bundet exempel: Utföra en beräkning för ett spel

Anta att du skriver ett mobilspel där du kan skada många fiender på skärmen genom att trycka på en knapp. Att utföra skadeberäkningen kan vara dyrt, och om du gör det i användargränssnittstråden skulle spelet se ut att pausa när beräkningen utförs!

Det bästa sättet att hantera detta är att starta en bakgrundstråd, som utför arbetet med och Task.Runväntar på resultatet med hjälp av await. Detta gör att användargränssnittet kan kännas smidigt när arbetet utförs.

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

Den här koden uttrycker tydligt avsikten med knappens klickhändelse, den kräver inte att du hanterar en bakgrundstråd manuellt och det gör den på ett icke-blockerande sätt.

Vad händer under täcket

På C#-sidan av saker omvandlar kompilatorn koden till en tillståndsdator som håller reda på saker som att ge körning när en await nås och återuppta körningen när ett bakgrundsjobb har slutförts.

För den teoretiskt lutande är detta en implementering av promise-modellen för asynkron.

Viktiga delar att förstå

  • Asynkron kod kan användas för både I/O-bunden och CPU-bunden kod, men olika för varje scenario.
  • Asynkron kod använder Task<T> och Task, som är konstruktioner som används för att modellera arbete som utförs i bakgrunden.
  • Nyckelordet async omvandlar en metod till en asynkron metod, vilket gör att du kan använda nyckelordet await i dess brödtext.
  • När nyckelordet await tillämpas pausar det anropande metoden och ger kontroll tillbaka till anroparen tills den väntade aktiviteten har slutförts.
  • await kan endast användas i en asynkron metod.

Identifiera processorbundet och I/O-bundet arbete

De första två exemplen i den här guiden visade hur du kan använda async och await för I/O-bundet och CPU-bundet arbete. Det är viktigt att du kan identifiera när ett jobb du behöver göra är I/O-bunden eller CPU-bunden eftersom det kan påverka kodens prestanda avsevärt och potentiellt kan leda till att vissa konstruktioner missbrukas.

Här är två frågor du bör ställa innan du skriver någon kod:

  1. Kommer koden att "vänta" på något, till exempel data från en databas?

    Om svaret är "ja" är ditt arbete I/O-bundet.

  2. Kommer koden att utföra en dyr beräkning?

    Om du svarade "ja" är ditt arbete CPU-bundet.

Om det arbete du har är I/O-bundet använder async du och awaitutanTask.Run. Du bör inte använda det parallella aktivitetsbiblioteket.

Om det arbete du har är CPU-bundet och du bryr dig om svarstider, använd async och await, men skapa bort arbetet på en annan tråd medTask.Run. Om arbetet är lämpligt för samtidighet och parallellitet bör du även överväga att använda det parallella aktivitetsbiblioteket.

Dessutom bör du alltid mäta körningen av koden. Du kan till exempel hamna i en situation där ditt CPU-bundna arbete inte är tillräckligt dyrt jämfört med omkostnaderna för kontextväxlar vid multitrådning. Varje val har sin kompromiss, och du bör välja rätt kompromiss för din situation.

Fler exempel

I följande exempel visas olika sätt att skriva asynkron kod i C#. De beskriver några olika scenarier som du kan stöta på.

Extrahera data från ett nätverk

Det här kodfragmentet laddar ned HTML-koden från den angivna URL:en och räknar antalet gånger strängen ".NET" inträffar i HTML-koden. Den använder ASP.NET för att definiera en webb-API-kontrollantmetod, som utför den här uppgiften och returnerar talet.

Kommentar

Om du planerar att utföra HTML-parsning i produktionskoden ska du inte använda reguljära uttryck. Använd ett parsningsbibliotek i stället.

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

Här är samma scenario skrivet för en Universell Windows-app, som utför samma uppgift när en knapp trycks ned:

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

Vänta tills flera uppgifter har slutförts

Du kan hamna i en situation där du behöver hämta flera datastycken samtidigt. API:et Task innehåller två metoder och Task.WhenAllTask.WhenAny, som gör att du kan skriva asynkron kod som utför en icke-blockerande väntan på flera bakgrundsjobb.

Det här exemplet visar hur du kan hämta User data för en uppsättning userIds.

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

Här är ett annat sätt att skriva detta mer kortfattat med LINQ:

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

Även om det är mindre kod bör du vara försiktig när du blandar LINQ med asynkron kod. Eftersom LINQ använder uppskjuten (lat) körning sker asynkrona anrop inte omedelbart som de gör i en foreach loop om du inte tvingar den genererade sekvensen att iterera med ett anrop till .ToList() eller .ToArray(). Exemplet ovan använder Enumerable.ToArray för att utföra frågan ivrigt och lagra resultaten i en matris. Det tvingar koden id => GetUserAsync(id) att köra och starta uppgiften.

Viktig information och råd

Med asynkron programmering finns det vissa detaljer att tänka på som kan förhindra oväntat beteende.

  • asyncmetoder måste ha ettawaitnyckelord i kroppen eller de kommer aldrig att ge!

    Detta är viktigt att tänka på. Om await den inte används i en metods brödtext async genererar C#-kompilatorn en varning, men koden kompileras och körs som om det vore en normal metod. Detta är otroligt ineffektivt eftersom tillståndsdatorn som genereras av C#-kompilatorn för async-metoden inte åstadkommer någonting.

  • Lägg till "Async" som suffix för varje asynkront metodnamn som du skriver.

    Det här är den konvention som används i .NET för att enklare skilja synkrona och asynkrona metoder åt. Vissa metoder som inte uttryckligen anropas av koden (till exempel händelsehanterare eller webbstyrenhetsmetoder) gäller inte nödvändigtvis. Eftersom de inte uttryckligen anropas av din kod är det inte lika viktigt att vara explicit om deras namngivning.

  • async voidska endast användas för händelsehanterare.

    async void är det enda sättet att tillåta asynkrona händelsehanterare att fungera eftersom händelser inte har returtyper (kan därför inte använda Task och Task<T>). All annan användning av async void följer inte TAP-modellen och kan vara svårt att använda, till exempel:

    • Undantag som genereras i en async void metod kan inte fångas utanför den metoden.
    • async void metoder är svåra att testa.
    • async void metoder kan orsaka dåliga biverkningar om anroparen inte förväntar sig att de ska vara asynkrona.
  • Gå försiktigt fram när du använder asynkrona lambdas i LINQ-uttryck

    Lambda-uttryck i LINQ använder uppskjuten körning, vilket innebär att kod kan köras vid en tidpunkt då du inte förväntar dig det. Införandet av blockerande uppgifter i detta kan enkelt leda till ett dödläge om det inte skrivs korrekt. Dessutom kan kapsling av asynkron kod som den här också göra det svårare att resonera om körningen av koden. Async och LINQ är kraftfulla men bör användas tillsammans så noggrant och tydligt som möjligt.

  • Skriv kod som väntar på aktiviteter på ett icke-blockerande sätt

    Att blockera den aktuella tråden som ett sätt att vänta tills en Task har slutförts kan leda till dödlägen och blockerade kontexttrådar och kan kräva mer komplex felhantering. Följande tabell innehåller vägledning om hur du hanterar väntan på uppgifter på ett icke-blockerande sätt:

    Använder du det här … I stället för det här... När du vill göra det här...
    await Task.Wait eller Task.Result Hämtar resultatet av en bakgrundsaktivitet
    await Task.WhenAny Task.WaitAny Väntar på att en uppgift ska slutföras
    await Task.WhenAll Task.WaitAll Väntar på att alla uppgifter ska slutföras
    await Task.Delay Thread.Sleep Väntar under en tidsperiod
  • Överväg att använda därValueTaskdet är möjligt

    Om du returnerar ett Task objekt från asynkrona metoder kan prestandaflaskhalsar introduceras i vissa sökvägar. Task är en referenstyp, så att använda den innebär att allokera ett objekt. Om en metod som deklareras med async modifieraren returnerar ett cachelagrat resultat eller slutförs synkront kan de extra allokeringarna bli en betydande tidskostnad i prestandakritiska kodavsnitt. Det kan bli kostsamt om dessa allokeringar sker i snäva loopar. Mer information finns i generaliserade asynkrona returtyper.

  • Överväg att användaConfigureAwait(false)

    En vanlig fråga är "när ska jag använda Task.ConfigureAwait(Boolean) metoden?". Metoden gör det möjligt för en Task instans att konfigurera dess awaiter. Detta är ett viktigt övervägande och att ange det felaktigt kan potentiellt få prestandakonsekvenser och till och med dödlägen. Mer information om ConfigureAwaitfinns i Vanliga frågor och svar om ConfigureAwait.

  • Skriva mindre tillståndskänslig kod

    Beror inte på tillståndet för globala objekt eller körningen av vissa metoder. I stället beror det bara på metodernas returvärden. Varför?

    • Kod blir lättare att resonera om.
    • Koden blir enklare att testa.
    • Det är mycket enklare att blanda asynkron och synkron kod.
    • Tävlingsförhållanden kan vanligtvis undvikas helt och hållet.
    • Beroende på returvärden är det enkelt att samordna asynkron kod.
    • (Bonus) det fungerar riktigt bra med beroendeinmatning.

Ett rekommenderat mål är att uppnå fullständig eller nästan fullständig referenstransparens i koden. Detta resulterar i en förutsägbar, testbar och underhållsbar kodbas.

Fullständigt exempel

Följande kod är den fullständiga texten i Program.cs-filen för exemplet.

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.

Andra resurser