Dela via


Asynkrona programmeringsscenarier

Om koden implementerar I/O-bundna scenarier för att stödja nätverksdatabegäranden, databasåtkomst eller skrivningar av filsystem är asynkron programmering den bästa metoden. Du kan också skriva asynkron kod för CPU-bundna scenarier som dyra beräkningar.

C# har en asynkron programmeringsmodell på språknivå som gör att du enkelt kan skriva asynkron kod utan att behöva jonglera motringningar eller följa ett bibliotek som stöder asynkron kod. Modellen följer det som kallas aktivitetsbaserat asynkront mönster (TAP).

Utforska den asynkrona programmeringsmodellen

Objekten Task och Task<T> representerar kärnan i asynkron programmering. Dessa objekt används för att modellera asynkrona åtgärder genom att stödja nyckelorden async och await. I de flesta fall är modellen ganska enkel för både I/O-bundna och CPU-bundna scenarier. Inuti en async metod:

  • I/O-bunden kod startar en åtgärd som representeras av ett Task- eller Task<T>-objekt i metoden async.
  • CPU-bunden kod startar en åtgärd på en bakgrundstråd med metoden Task.Run.

I båda fallen representerar en aktiv Task en asynkron åtgärd som kanske inte är fullständig.

Nyckelordet await är där magin händer. Det ger kontroll till anroparen av metoden som innehåller await-uttrycket och gör slutligen att användargränssnittet kan vara dynamiskt eller att en tjänst är elastisk. Även om det finns sätt att närma sig asynkron kod än att använda async- och await-uttryck, fokuserar den här artikeln på konstruktionerna på språknivå.

Kommentar

Några exempel som presenteras i den här artikeln använder klassen System.Net.Http.HttpClient för att ladda ned data från en webbtjänst. I exempelkoden är s_httpClient-objektet ett statiskt fält av typen Program klass:

private static readonly HttpClient s_httpClient = new();

Mer information finns i fullständiga exempelkoden i slutet av den här artikeln.

Granska underliggande begrepp

När du implementerar asynkron programmering i C#-koden omvandlar kompilatorn programmet till en tillståndsdator. Den här konstruktionen spårar olika åtgärder och tillstånd i koden, till exempel att avbryta körningen när koden når ett await uttryck och återuppta körningen när ett bakgrundsjobb avslutas.

När det gäller datavetenskapsteori är asynkron programmering en implementering av Promise-modellen för asynkron.

I den asynkrona programmeringsmodellen finns det flera viktiga begrepp att förstå:

  • Du kan använda asynkron kod för både I/O-bunden och CPU-bunden kod, men implementeringen är annorlunda.
  • Asynkron kod använder Task<T> och Task objekt som konstruktioner för att modellera arbete som körs i bakgrunden.
  • Nyckelordet async deklarerar en metod som en asynkron metod, vilket gör att du kan använda nyckelordet await i metodtexten.
  • När du använder nyckelordet await pausar koden anropande metoden och ger kontroll tillbaka till anroparen tills uppgiften har slutförts.
  • Du kan bara använda uttrycket await i en asynkron metod.

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

I det här exemplet, när användaren väljer en knapp, laddar appen ned data från en webbtjänst. Du vill inte blockera användargränssnittstråden för appen under nedladdningsprocessen. Följande kod utför den här uppgiften:

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: Kör spelberäkning

I nästa exempel orsakar ett mobilspel att flera agenter på skärmen tar skada som svar på ett knapptryck. Det kan vara dyrt att utföra skadeberäkningen. Om du kör beräkningen i användargränssnittstråden kan det orsaka interaktionsproblem med visning och användargränssnitt under beräkningen.

Det bästa sättet att hantera uppgiften är att starta en bakgrundstråd för att slutföra arbetet med metoden Task.Run. Operationen genererar ett await-uttryck. Åtgärden återupptas när aktiviteten är klar. Med den här metoden kan användargränssnittet köras smidigt medan arbetet slutförs i bakgrunden.

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

Koden uttrycker tydligt avsikten för händelsen av knappen Clicked. Det kräver inte att du hanterar en bakgrundstråd manuellt, och den slutför uppgiften på ett icke-blockerande sätt.

Identifiera cpu-bundna och I/O-bundna scenarier

Föregående exempel visar hur du använder async modifierare och await uttryck för I/O-bundet och CPU-bundet arbete. Ett exempel för varje scenario visar hur koden skiljer sig beroende på var åtgärden är bunden. För att förbereda för implementeringen måste du förstå hur du identifierar när en åtgärd är I/O-bunden eller CPU-bunden. Ditt implementeringsval kan avsevärt påverka kodens prestanda och potentiellt leda till felaktig användning av konstruktioner.

Det finns två primära frågor att ta itu med innan du skriver någon kod:

Fråga Scenario Genomförande
Ska koden vänta på ett resultat eller en åtgärd, till exempel data från en databas? I/O-begränsad Använd async-modifieraren och await-uttrycket utan att metoden Task.Run.

Undvik att använda det parallella aktivitetsbiblioteket.
Ska koden köra en dyr beräkning? CPU-bundna Använd async modifierare och await uttryck, men fördela arbetet till en annan tråd med metoden Task.Run. Den här metoden åtgärdar problem med processorresponsivitet.

Om arbetet är lämpligt för samtidighet och parallellitet bör du även överväga att använda det parallella aktivitetsbiblioteket.

Kontrollera alltid kodens körning. Du kanske upptäcker att ditt CPU-bundna arbete inte är tillräckligt dyrt jämfört med omkostnaderna för kontextväxlar vid multitrådning. Alla val har kompromisser. Välj rätt kompromiss för din situation.

Utforska andra exempel

Exemplen i det här avsnittet visar flera sätt att skriva asynkron kod i C#. De beskriver några scenarier som du kan stöta på.

Extrahera data från ett nätverk

Följande kod laddar ned HTML från en viss URL och räknar antalet gånger strängen ".NET" inträffar i HTML-koden. Koden använder ASP.NET för att definiera en web-API-kontrollantmetod som utför uppgiften och returnerar antalet.

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

Du kan skriva liknande kod för en universell Windows-app och utföra inventeringsaktiviteten efter en knapptryckning:

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

Vänta tills flera uppgifter har slutförts

I vissa scenarier måste koden hämta flera datastycken samtidigt. De Task API:erna innehåller metoder som gör att du kan skriva asynkron kod som utför en icke-blockerande väntan på flera bakgrundsjobb:

I följande exempel visas hur du kan hämta User objektdata för en uppsättning userId objekt.

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

Du kan skriva den här koden mer kortfattat med hjälp av 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 du skriver mindre kod med LINQ bör du vara försiktig när du blandar LINQ med asynkron kod. LINQ använder uppskjuten (eller lat) körning. Asynkrona anrop sker inte omedelbart som i en foreach-loop, såvida du inte tvingar den genererade sekvensen att iterera med ett anrop till metoden .ToList() eller .ToArray(). I det här exemplet används metoden Enumerable.ToArray för att utföra frågan ivrigt och lagra resultaten i en matris. Den här metoden tvingar id => GetUserAsync(id)-instruktionen att köra och initiera uppgiften.

Granska överväganden för asynkron programmering

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

Använda await i async()-metodtexten

När du använder async-modifieraren bör du inkludera ett eller flera await uttryck i metodtexten. Om kompilatorn inte stöter på ett await uttryck kan metoden inte ge resultat. Även om kompilatorn genererar en varning kompilerar koden fortfarande och kompilatorn kör metoden. Tillståndsdatorn som genereras av C#-kompilatorn för den asynkrona metoden åstadkommer ingenting, så hela processen är mycket ineffektiv.

Lägg till suffixet "Async" i asynkrona metodnamn

.NET-stilkonventionen är att lägga till suffixet "Async" i alla asynkrona metodnamn. Den här metoden gör det enklare att skilja mellan synkrona och asynkrona metoder. Vissa metoder som inte uttryckligen anropas av din kod (till exempel händelsehanterare eller webbstyrenhetsmetoder) gäller inte nödvändigtvis i det här scenariot. Eftersom dessa objekt inte uttryckligen anropas av koden är det inte lika viktigt att använda explicit namngivning.

Returnera 'async void' endast från händelsehanterare

Händelsehanterare måste deklarera void returtyper och kan inte använda eller returnera Task och Task<T> objekt som andra metoder gör. När du skriver asynkrona händelsehanterare måste du använda async-modifieraren på en void returneringsmetod för hanterarna. Andra implementeringar av async void returnerade metoder följer inte TAP-modellen och kan innebära utmaningar:

  • 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 negativa biverkningar om anroparen inte förväntar sig att de ska vara asynkrona

Var försiktig med asynkrona lambdas i LINQ

Det är viktigt att vara försiktig när du implementerar asynkrona lambdas i LINQ-uttryck. Lambda-uttryck i LINQ använder uppskjuten körning, vilket innebär att koden kan köras vid en oväntad tidpunkt. Införandet av blockerande uppgifter i det här scenariot kan enkelt leda till ett dödläge om koden inte skrivs korrekt. Dessutom kan kapsling av asynkron kod också göra det svårt att tänka igenom körningen av koden. Async och LINQ är kraftfulla, men dessa tekniker bör användas tillsammans så noggrant och tydligt som möjligt.

Ge vika för uppgifter på ett icke-blockerande sätt

Om programmet behöver resultatet av en uppgift skriver du kod som implementerar await-uttrycket på ett icke-blockerande sätt. Om du blockerar den aktuella tråden som ett sätt att vänta synkront på att ett Task objekt ska slutföras kan det leda till dödlägen och blockerade kontexttrådar. Den här programmeringsmetoden kan kräva mer komplex felhantering. Följande tabell innehåller vägledning om hur åtkomsten resulterar från aktiviteter på ett icke-blockerande sätt:

Aktivitetsscenario Aktuell kod Ersätt med "await"
Hämta resultatet av en bakgrundsaktivitet Task.Wait eller Task.Result await
Fortsätt när en aktivitet slutförs Task.WaitAny await Task.WhenAny
Fortsätt när alla uppgifter är slutförda Task.WaitAll await Task.WhenAll
Fortsätt efter en viss tid Thread.Sleep await Task.Delay

Överväg att använda ValueTask-typ

När en asynkron metod returnerar ett Task-objekt kan prestandaflaskhalsar uppstå i vissa sökvägar. Eftersom Task är en referenstyp, allokeras ett Task-objekt från högen. Om en metod som deklareras med async-modifieraren returnerar ett cachelagrat resultat eller slutförs synkront kan de extra allokeringarna medföra betydande tidskostnader i prestandakritiska kodavsnitt. Det här scenariot kan bli kostsamt när allokeringarna sker i snäva loopar. Mer information finns i generaliserade asynkrona returtyper.

Förstå när du ska ange ConfigureAwait(false)

Utvecklare frågar ofta när de ska använda Task.ConfigureAwait(Boolean) boolesk. Med det här API:et kan en Task instans konfigurera kontexten för tillståndsdatorn som implementerar alla await uttryck. När det booleska värdet inte är korrekt inställt kan prestanda försämras eller så kan dödlägen uppstå. Mer information finns i ConfigureAwait FAQ.

Skriva mindre tillståndskänslig kod

Undvik att skriva kod som är beroende av tillståndet för globala objekt eller körningen av vissa metoder. I stället beror det bara på metodernas returvärden. Det finns många fördelar med att skriva kod som är mindre tillståndskänslig:

  • Enklare att resonera om kod
  • Enklare att testa kod
  • Enklare att blanda asynkron och synkron kod
  • Kunna undvika tävlingsförhållanden i kod
  • Enkelt att samordna asynkron kod som är beroende av returvärden
  • (Bonus) Fungerar bra med beroendeinmatning i kod

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

Granska det fullständiga exemplet

Följande kod representerar det fullständiga exemplet, som är tillgängligt i Program.cs exempelfilen.

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.