Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Scénáře asynchronního programování
Článek
22. 03. 2025
Pokud váš kód implementuje scénáře vázané na vstupně-výstupní operace pro podporu síťových požadavků na data, přístupu k databázi nebo čtení a zápisů systému souborů, je nejlepší přístup asynchronního programování. Můžete také napsat asynchronní kód pro scénáře vázané na procesor, jako jsou nákladné výpočty.
Jazyk C# má asynchronní programovací model na úrovni jazyka, který umožňuje snadno psát asynchronní kód, aniž byste museli interpretovat zpětná volání nebo odpovídat knihovně, která podporuje asynchrony. Model sleduje, co se označuje jako asynchronní vzor založený na úlohách (TAP).
Prozkoumání asynchronního programovacího modelu
Objekty Task a Task<T> představují jádro asynchronního programování. Tyto objekty se používají k modelování asynchronních operací podporou async a await klíčových slov. Ve většině případů je model poměrně jednoduchý pro scénáře vázané na vstupně-výstupní operace i procesor. Uvnitř metody async:
vstupně-výstupní kód spustí operaci reprezentovanou objektem Task nebo Task<T> v rámci metody async.
Kód vázaný na procesor spouští operaci na pozadí pomocí metody ve vlákně Task.Run.
V obou případech aktivní Task představuje asynchronní operaci, která nemusí být dokončena.
Klíčové await slovo je místo, kde se magie děje. Poskytuje kontrolu volajícímu metody, která obsahuje výraz await, a nakonec umožňuje, aby uživatelské rozhraní reagovalo nebo služba byla elastická. I když existují způsoby, přistupovat k asynchronnímu kódu jiným způsobem než pomocí výrazů async a await, tento článek se zaměřuje na konstrukce na úrovni jazyka.
Poznámka
Některé příklady uvedené v tomto článku používají třídu System.Net.Http.HttpClient ke stažení dat z webové služby. V ukázkovém kódu je objekt s_httpClient statické pole typu Program třídy:
Při implementaci asynchronního programování v kódu jazyka C# kompilátor transformuje váš program na stavový počítač. Tento konstrukt sleduje různé operace a stav ve vašem kódu, jako například uvolnění vykonávání, když kód dosáhne výrazu await, a obnovení vykonávání, když je dokončena úloha na pozadí.
Z hlediska teorie počítačových věd je asynchronní programování implementací modelu Promise asynchrony.
V asynchronním programovacím modelu existuje několik klíčových konceptů, které je potřeba pochopit:
Asynchronní kód můžete použít pro vstupně-výstupní i procesorový kód, ale implementace se liší.
Asynchronní kód používá Task<T> a Task objekty jako konstrukty k modelování práce běžící na pozadí.
Klíčové slovo async deklaruje metodu jako asynchronní metodu, která umožňuje použít klíčové slovo await v těle metody.
Když použijete klíčové slovo await, kód pozastaví volající metodu a vrátí řízení zpět volajícímu, dokud se úkol dokončí.
Výraz await můžete použít pouze v asynchronní metodě.
Příklad vázané na vstupně-výstupní operace: Stažení dat z webové služby
Když uživatel v tomto příkladu vybere tlačítko, aplikace stáhne data z webové služby. Během procesu stahování nechcete blokovat vlákno uživatelského rozhraní aplikace. Následující kód provede tuto úlohu:
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 zátěže na procesor: Výpočet hry
V dalším příkladu mobilní hra udělí poškození několika agentům na obrazovce v reakci na stisknutí tlačítka. Výpočet poškození může být nákladný. Spuštění výpočtu ve vlákně uživatelského rozhraní může během výpočtu způsobit problémy se zobrazením a interakcí s uživatelským rozhraním.
Nejlepším způsobem, jak zpracovat úlohu, je spustit vlákno na pozadí pro dokončení práce metodou Task.Run. Operace se provádí pomocí výrazu await. Operace se obnoví po dokončení úkolu. Tento přístup umožňuje, aby uživatelské rozhraní fungovalo hladce, zatímco se práce dokončí na pozadí.
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);
};
Kód jasně vyjadřuje záměr události tlačítka Clicked. Nevyžaduje ruční správu vlákna na pozadí a úkol dokončí neblokujícím způsobem.
Rozpoznejte scénáře vázané na procesor a vstupně-výstupně vázané
Předchozí příklady ukazují, jak použít modifikátor async a výraz await pro práci vázané na vstupně-výstupní operace a procesor. Příklad pro každý scénář ukazuje, jak se kód liší podle toho, kde je operace svázaná. Pokud se chcete připravit na implementaci, musíte pochopit, jak zjistit, kdy je operace vázaná na vstupně-výstupní operace nebo na procesor. Vaše volba implementace může výrazně ovlivnit výkon kódu a potenciálně vést k nesprávnému použití konstruktorů.
Před napsáním jakéhokoli kódu je potřeba řešit dvě hlavní otázky:
Otázka
Scénář
Implementace
Měl by kód čekat na výsledek nebo akci, například data z databáze?
omezený vstupně-výstupními operacemi
Použijte modifikátor async a výraz awaitbez metodyTask.Run.
Nepoužívejte paralelní knihovnu úloh.
Měl by kód spustit nákladný výpočet?
vázané na procesor
Použijte modifikátor async a výraz await, ale spusťte práci na jiném vlákně pomocí metody Task.Run. Tento přístup řeší problémy s odezvou procesoru.
Pokud je práce vhodná pro souběžnost a paralelismus, zvažte také použití paralelní knihovny úloh.
Vždy změřte provádění kódu. Můžete zjistit, že vaše práce vázaná na procesor není dostatečně nákladná ve srovnání s režií kontextových přepínačů při používání více vláken. Každá volba má kompromisy. Vyberte správný kompromis pro vaši situaci.
Prozkoumání dalších příkladů
Příklady v této části ukazují několik způsobů psaní asynchronního kódu v jazyce C#. Pokrývají několik scénářů, se kterými se můžete setkat.
Extrakce dat ze sítě
Následující kód stáhne kód HTML z dané adresy URL a spočítá, kolikrát se řetězec ".NET" vyskytuje v html. Kód používá ASP.NET k definování metody kontroleru webového rozhraní API, která provádí úlohu a vrací počet.
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;
}
Podobný kód pro univerzální aplikaci pro Windows můžete napsat a provést úlohu počítání po 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.
// 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;
}
Počkejte na dokončení více úkolů.
V některých scénářích musí kód současně načíst několik částí dat. Rozhraní API Task poskytují metody, které umožňují psát asynchronní kód s možností čekání bez blokování na více úloh na pozadí.
Následující příklad ukazuje, jak můžete získat data pro objekt User u sady objektů userId.
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);
}
I když píšete méně kódu pomocí LINQ, při kombinování LINQ s asynchronním kódem buďte opatrní. LINQ používá odložené (nebo líné) provádění. Asynchronní volání se nedějí okamžitě stejně jako ve smyčce foreach, pokud nevynutíte, aby vygenerovaná sekvence iterovala voláním metody .ToList() nebo .ToArray(). Tento příklad používá metodu Enumerable.ToArray k rychlému provedení dotazu a uložení výsledků do pole. Tento přístup vynutí spuštění příkazu id => GetUserAsync(id) a zahájí úlohu.
Projděte si důležité informace o asynchronním programování.
Při asynchronním programování je potřeba mít na paměti několik podrobností, které mohou zabránit neočekávanému chování.
Použití operátoru await v textu metody async()
Pokud použijete modifikátor async, měli byste do těla metody zahrnout jeden nebo více výrazů await. Pokud kompilátor nenarazí na výraz await, metoda se nezdaří. Přestože kompilátor vygeneruje upozornění, kód se stále zkompiluje a kompilátor spustí metodu. Stavový počítač vygenerovaný kompilátorem jazyka C# pro asynchronní metodu nic nedělá, takže celý proces je vysoce neefektivní.
Přidání přípony "Async" k názvům asynchronních metod
Konvence stylu .NET je přidání přípony "Async" do všech názvů asynchronních metod. Tento přístup pomáhá snadněji rozlišovat mezi synchronními a asynchronními metodami. Některé metody, které nejsou explicitně volána vaším kódem (například obslužnými rutinami událostí nebo metodami webového kontroleru), se v tomto scénáři nemusí nutně použít. Protože tyto položky nejsou explicitně volána vaším kódem, použití explicitního pojmenování není tak důležité.
Vrátit 'async void' pouze z obslužných metod událostí
Obslužné rutiny událostí musí deklarovat void návratové typy a nemůžou používat ani vracet Task a Task<T> objekty jako jiné metody. Při psaní asynchronních obslužných rutin událostí je nutné použít modifikátor async na metodu vracející void pro tyto rutin. Jiné implementace async void návratových metod nedodržují model TAP a můžou představovat výzvy:
Výjimky vyvolané v metodě async void nelze zachytit mimo tuto metodu.
async void metody jsou obtížné testovat
async void metody můžou způsobit negativní vedlejší účinky, pokud volající neočekává, že budou asynchronní.
Opatrně používejte asynchronní lambdy v LINQ
Při implementaci asynchronních lambda ve výrazech LINQ je důležité postupovat opatrně. Výrazy lambda v LINQ používají odložené spuštění, což znamená, že kód se může spustit v neočekávanou dobu. Zavedení blokujících úloh do tohoto scénáře může snadno vést k zablokování, pokud kód není správně napsaný. Kromě toho může vnoření asynchronního kódu také ztížit pochopení jeho provádění. Asynchronní a LINQ jsou výkonné, ale tyto techniky by se měly používat co nejopatrněji a nejjasněji.
Vzdát kontrolu úkolům neblokujícím způsobem
Pokud váš program potřebuje výsledek úkolu, napište kód, který implementuje výraz await neblokujícím způsobem. Blokování aktuálního vlákna jako prostředku k synchronnímu čekání na dokončení položky Task může vést k zablokování a blokovaným kontextovým vláknům. Tento programovací přístup může vyžadovat složitější zpracování chyb. Následující tabulka poskytuje pokyny, jak neblokujícím způsobem získat výsledky z úkolů:
Scénář úlohy
Aktuální kód
Nahradit výrazem 'await'
Načtení výsledku úlohy na pozadí
Task.Wait nebo Task.Result
await
Pokračovat po dokončení všech úkolů
Task.WaitAny
await Task.WhenAny
Pokračovat, až všechny úkoly dokončí
Task.WaitAll
await Task.WhenAll
Pokračovat po určité době
Thread.Sleep
await Task.Delay
Zvažte použití typu ValueTask.
Když asynchronní metoda vrátí objekt Task, mohou být v určitých cestách zavedeny kritické body výkonu. Protože Task je referenční typ, je objekt Task přidělen z haldy. Pokud metoda deklarovaná pomocí modifikátoru async vrátí výsledek uložený v mezipaměti nebo se dokončí synchronně, mohou dodatečné alokace značně ovlivnit časové náklady v částech kódu kritických pro výkon. Tento scénář může být nákladný, když k přidělování dochází v těsných smyčkách. Další informace naleznete v tématu generalizované asynchronní návratové typy.
Vysvětlení, kdy nastavit ConfigureAwait(false)
Vývojáři se často ptají, kdy používat logickou hodnotu Task.ConfigureAwait(Boolean). Toto rozhraní API umožňuje instanci Task nakonfigurovat kontext pro stavový počítač, který implementuje libovolný výraz await. Pokud není booleovská hodnota nastavená správně, může dojít ke snížení výkonu nebo zásekům. Další informace najdete v tématu Nejčastější dotazy ke konfiguraci Await.
Pište méně stavový kód
Vyhněte se psaní kódu, který závisí na stavu globálních objektů nebo provádění určitých metod. Místo toho závisí pouze na návratových hodnotách metod. Psaní kódu, který je méně stavový, má mnoho výhod:
Jednodušší zdůvodnění kódu
Jednodušší testování kódu
Jednodušší kombinování asynchronního a synchronního kódu
Možnost vyhnout se kritickým situacím v kódu
Jednoduché na koordinaci asynchronního kódu, který závisí na vrácených hodnotách
(Bonus) Dobře funguje s injektáží závislostí v kódu
Doporučeným cílem je dosáhnout úplné nebo téměř úplné referenční transparentnosti v kódu. Výsledkem tohoto přístupu je předvídatelný, testovatelný a udržovatelný základ kódu.
Kontrola kompletního příkladu
Následující kód představuje úplný příklad, který je k dispozici v ukázkovém souboru Program.cs.
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.
Zdroj tohoto obsahu najdete na GitHubu, kde můžete také vytvářet a kontrolovat problémy a žádosti o přijetí změn. Další informace najdete v našem průvodci pro přispěvatele.
Zpětná vazba k produktu
.NET
.NET
je open source projekt. Vyberte odkaz pro poskytnutí zpětné vazby:
Naučte se používat Task.WhenAny v jazyce C# ke spuštění více úloh a zpracování jejich výsledků hned, jak se dokončí, a ne ke zpracování v pořadí, v jakém byly zahájeny.
Tento pokročilý kurz ukazuje, jak generovat a využívat asynchronní streamy. Asynchronní datové proudy poskytují přirozenější způsob práce se sekvencemi dat, která se můžou generovat asynchronně.
Vytvářejte ucelená řešení v Microsoft Azure pro vytváření funkcí Azure, implementaci a správu webových aplikací, vývoj řešení využívajících úložiště Azure a další.