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
neboTask<T>
uvnitř metodyasync
. - 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>
aTask
které 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žítawait
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:
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.
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
a await
bez Task.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 vytváříte práci na jiném vlákně s Task.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.WhenAll Task.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 userId
s.
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í.
async
metody musí mítawait
klíčové slovo ve svém těle, nebo oni nikdy nepřinesou!To je důležité mít na paměti. Pokud
await
se v těleasync
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 void
by 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žívatTask
aTask<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í.
- Výjimky vyvolané v
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
neboTask.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átoremasync
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 najdeteConfigureAwait
v 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/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.