Scénáře asynchronního programování

Pokud máte nějaké vstupně-výstupní požadavky (například vyžádání dat ze sítě, přístup k databázi nebo čtení a zápis do systému souborů), budete chtít využít asynchronní programování. Můžete mít také kód vázaný na procesor, například provést nákladný výpočet, což je také vhodný scénář pro psaní asynchronního kódu.

Jazyk C# má asynchronní programovací model na úrovni jazyka, který umožňuje snadno psát asynchronní kód, aniž by bylo nutné interpretovat zpětná volání nebo odpovídat knihovně, která podporuje asynchrony. Následuje to, co se označuje jako asynchronní vzor založený na úlohách (TAP).

Přehled asynchronního modelu

Jádrem asynchronního programování jsou Task objekty, Task<T> které modelují asynchronní operace. Podporují je klíčová async slova a await klíčová slova. Model je ve většině případů poměrně jednoduchý:

  • V případě V/V vázaného kódu čekáte na operaci, která vrací metodu Task nebo Task<T> uvnitř metody async .
  • U kódu vázaného na procesor čekáte na operaci, která se spustí ve vlákně na pozadí s metodou Task.Run .

Klíčové await slovo je místo, kde se magie děje. Poskytuje kontrolu volajícímu metody, která provedla await, a nakonec umožňuje, aby uživatelské rozhraní reagovalo nebo služba byla elastická. I když existují způsoby, jak přistupovat k asynchronnímu kódu jiným než async a await, tento článek se zaměřuje na konstrukce na úrovni jazyka.

Poznámka:

V některých z následujících příkladů System.Net.Http.HttpClient se třída používá ke stažení některých dat z webové služby. Objekt s_httpClient použitý v těchto příkladech je statické pole Program třídy (zkontrolujte úplný příklad):

private static readonly HttpClient s_httpClient = new();

Příklad vázané na vstupně-výstupní operace: Stažení dat z webové služby

Možná budete muset stáhnout některá data z webové služby, když je tlačítko stisknuto, ale nechcete blokovat vlákno uživatelského rozhraní. Dá se to udělat takto:

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

Kód vyjadřuje záměr (stahování dat asynchronně) bez toho, aby při interakci s Task objekty zabředl.

Příklad vázané na procesor: Provedení výpočtu hry

Řekněme, že píšete mobilní hru, kde stisknutí tlačítka může způsobit poškození mnoha nepřátel na obrazovce. Provedení výpočtu poškození může být nákladné a jeho provedení ve vlákně uživatelského rozhraní způsobí, že se hra při provádění výpočtu pozastaví.

Nejlepším způsobem, jak to vyřešit, je spustit vlákno na pozadí, které pracuje pomocí Task.Run, a očekávat jeho výsledek pomocí await. Díky tomu se uživatelské rozhraní bude cítit hladce při práci.

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

Tento kód jasně vyjadřuje záměr události kliknutí na tlačítko, nevyžaduje ruční správu vlákna na pozadí a dělá to neblokujícím způsobem.

Co se stane pod kryty

Na straně jazyka C# kompilátor transformuje kód na stavový počítač, který sleduje věci, jako je provedení při await dosažení a obnovení provádění po dokončení úlohy na pozadí.

Pro teoreticky sklon je to implementace modelu příslibu asynchrony.

Klíčové části, které je potřeba pochopit

  • Asynchronní kód lze použít pro vstupně-výstupní i procesorově vázané kódy, ale pro každý scénář se liší.
  • Asynchronní kód používá Task<T> a Taskkteré jsou konstruktory používané k modelování práce prováděné na pozadí.
  • Klíčové async slovo změní metodu na asynchronní metodu, která umožňuje použít await klíčové slovo v jeho těle.
  • Při použití klíčového await slova pozastaví volající metodu a vrátí řízení zpět volajícímu, dokud nebude dokončena očekávaná úloha.
  • await lze použít pouze uvnitř asynchronní metody.

Rozpoznávání práce vázané na procesor a vstupně-výstupní operace

První dva příklady tohoto průvodce ukázaly, jak můžete použít a await jak pracovat async vázaný na vstupně-výstupní operace a procesor. Je to klíč, který můžete identifikovat, kdy je úloha, kterou potřebujete udělat, vázaná na vstupně-výstupní operace nebo procesor, protože může výrazně ovlivnit výkon vašeho kódu a může potenciálně vést k chybnému použití určitých konstruktorů.

Tady jsou dvě otázky, které byste měli položit předtím, než napíšete jakýkoli kód:

  1. Bude váš kód "čekat" na něco, například na data z databáze?

    Pokud je odpověď ano, pak je vaše práce vázaná na vstupně-výstupní operace.

  2. Bude váš kód provádět nákladné výpočty?

    Pokud jste odpověděli na "ano", vaše práce je vázána na procesor.

Pokud máte práci vázanou na vstupně-výstupní operace, použijte async ji aawait bezTask.Run. Neměli byste používat paralelní knihovnu úloh.

Pokud máte práci vázanou na procesor a záleží na odezvě, použití async a await, ale třete práci na jiném vlákně sTask.Run. Pokud je práce vhodná pro souběžnost a paralelismus, zvažte také použití paralelní knihovny úloh.

Kromě toho byste měli vždy měřit provádění kódu. Můžete se například setkat v situaci, kdy vaše práce vázaná na procesor není dostatečně nákladná v porovnání s režií kontextových přepínačů při multithreadingu. Každá volba má svůj kompromis a měli byste vybrat správný kompromis pro vaši situaci.

Další příklady

Následující příklady ukazují různé způsoby psaní asynchronního kódu v jazyce C#. Pokrývají několik různých scénářů, se kterým se můžete setkat.

Extrakce dat ze sítě

Tento fragment kódu stáhne kód HTML z dané adresy URL a spočítá, kolikrát se řetězec ".NET" vyskytuje v html. Používá ASP.NET k definování metody kontroleru webového rozhraní API, která provádí tuto úlohu a vrací číslo.

Poznámka:

Pokud plánujete parsování HTML v produkčním kódu, nepoužívejte regulární výrazy. Místo toho použijte knihovnu pro analýzu.

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

Tady je stejný scénář napsaný pro univerzální aplikaci pro Windows, který provádí stejnou úlohu při stisknutí tlačítka:

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

Počkejte na dokončení více úkolů.

Můžete se setkat v situaci, kdy potřebujete současně načíst více částí dat. Rozhraní Task API obsahuje dvě metody a Task.WhenAllTask.WhenAnyumožňují psát asynchronní kód, který provádí neblokující čekání na více úloh na pozadí.

Tento příklad ukazuje, jak můžete získat User data pro sadu 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);
}

Tady je další způsob, jak to stručně napsat pomocí LINQ:

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

I když je méně kódu, při kombinování LINQ s asynchronním kódem buďte opatrní. Vzhledem k tomu, že LINQ používá odložené (opožděné) spuštění, asynchronní volání nebudou foreach probíhat okamžitě jako ve smyčce, pokud nevynutíte, aby vygenerovaná sekvence iterovala voláním .ToList() nebo .ToArray(). Výše uvedený příklad používá Enumerable.ToArray k dychtivým provedení dotazu a uložení výsledků do pole. Tím vynutíte spuštění a spuštění úlohy kódem id => GetUserAsync(id) .

Důležité informace a rady

Při asynchronním programování je potřeba mít na paměti některé podrobnosti, které mohou zabránit neočekávanému chování.

  • asyncmetody musí mítawaitv těle klíčové slovo, nebo nikdy nepřinesou!

    To je důležité mít na paměti. Pokud await se v těle async metody nepoužívá, kompilátor jazyka C# vygeneruje upozornění, ale kód se zkompiluje a spustí, jako by šlo o normální metodu. To je neuvěřitelně neefektivní, protože stavový počítač generovaný kompilátorem jazyka C# pro asynchronní metodu nic neuskuteční.

  • Přidejte "Async" jako příponu každého názvu asynchronní metody, kterou napíšete.

    Toto je konvence používaná v .NET k snadnějšímu rozlišení synchronních a asynchronních metod. Některé metody, které váš kód explicitně nevolá (například obslužné rutiny událostí nebo metody webového kontroleru), se nemusí nutně použít. Protože nejsou explicitně volány vaším kódem, explicitní označení jejich názvů není tak důležité.

  • async voidby se měla používat pouze pro obslužné rutiny událostí.

    async void je jediný způsob, jak povolit, aby asynchronní obslužné rutiny událostí fungovaly, protože události nemají návratové typy (proto nemohou využívat Task a Task<T>). Jakékoli jiné použití async void modelu TAP neodpovídá a může být náročné, například:

    • Výjimky vyvolané v async void metodě nelze zachytit mimo tuto metodu.
    • async void metody jsou obtížné testovat.
    • async void metody můžou způsobit špatné vedlejší účinky, pokud volající neočekává, že budou asynchronní.
  • Při použití asynchronních lambda ve výrazech LINQ pečlivě přečtené

    Výrazy lambda v LINQ používají odložené spuštění, což znamená, že kód může být spuštěný najednou, když ho neočekáváte. Zavedení blokujících úkolů do tohoto úkolu může snadno vést k zablokování, pokud není zapsáno správně. Kromě toho může vnoření asynchronního kódu, jako je tento, ztížit také důvod spuštění kódu. Asynchronní a LINQ jsou výkonné, ale měly by se používat co nejdůrazněji a co nejjasněji.

  • Napsání kódu, který čeká na úkoly neblokujícím způsobem

    Blokování aktuálního vlákna jako prostředku k čekání na Task dokončení může vést k zablokování a blokovaným kontextovým vláknům a může vyžadovat složitější zpracování chyb. Následující tabulka obsahuje pokyny, jak řešit čekání na úkoly neblokujícím způsobem:

    Postup... Místo toho... Když to chcete udělat...
    await Task.Wait nebo Task.Result Načtení výsledku úlohy na pozadí
    await Task.WhenAny Task.WaitAny Čekání na dokončení libovolného úkolu
    await Task.WhenAll Task.WaitAll Čekání na dokončení všech úkolů
    await Task.Delay Thread.Sleep Čekání na určité časové období
  • Zvažte použitíValueTask, pokud je to možné

    Vrácení objektu Task z asynchronních metod může v určitých cestách zavádět kritické body výkonu. Task je typ odkazu, takže použití znamená přidělení objektu. V případech, kdy metoda deklarovaná s modifikátorem async vrátí výsledek uložený v mezipaměti nebo se dokončí synchronně, můžou se dodatečné přidělení stát významnými časovými náklady v částech kódu kritických pro výkon. Pokud k těmto přidělením dojde v těsné smyčce, může to být nákladné. Další informace naleznete v tématu generalizované asynchronní návratové typy.

  • Zvažte použitíConfigureAwait(false)

    Běžnou otázkou je, kdy použít metodu Task.ConfigureAwait(Boolean) ? Tato metoda umožňuje Task instanci nakonfigurovat její operátor awaiter. Jedná se o důležitý faktor a nesprávné nastavení může mít vliv na výkon a dokonce i zablokování. Další informace najdete ConfigureAwaitv nejčastějších dotazech ke konfiguraci Await.

  • Psaní méně stavových kódů

    Nezávisí na stavu globálních objektů ani na provádění určitých metod. Místo toho závisí pouze na návratových hodnotách metod. Proč?

    • Kód bude jednodušší zdůvodnění.
    • Testování kódu bude snazší.
    • Kombinování asynchronního a synchronního kódu je mnohem jednodušší.
    • Podmínky závodu se obvykle dají úplně vyhnout.
    • V závislosti na návratových hodnotách je koordinace asynchronního kódu jednoduchá.
    • (Bonus) funguje s injektáží závislostí opravdu dobře.

Doporučeným cílem je dosáhnout úplné nebo téměř úplné referenční transparentnosti v kódu. Výsledkem bude předvídatelný, testovatelný a udržovatelný základ kódu.

Kompletní příklad

Následující kód je úplný text souboru Program.cs příkladu.

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/xamarin"
    };

    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/xamarin: 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.

Další prostředky