Asynchrone programmeerscenario's
Als uw code I/O-gebonden scenario's implementeert ter ondersteuning van netwerkgegevensaanvragen, databasetoegang of lees-/schrijfbewerkingen van het bestandssysteem, is asynchrone programmering de beste aanpak. U kunt ook asynchrone code schrijven voor CPU-gebonden scenario's, zoals dure berekeningen.
C# heeft een asynchroon programmeermodel op taalniveau waarmee u eenvoudig asynchrone code kunt schrijven zonder dat u callbacks hoeft te gebruiken of aan een bibliotheek moet voldoen die asynchroon ondersteunt. Het model volgt wat bekend staat als het op taak gebaseerde asynchrone patroon (TAP).
Het asynchrone programmeermodel verkennen
De Task
- en Task<T>
-objecten vertegenwoordigen de kern van asynchrone programmering. Deze objecten worden gebruikt om asynchrone bewerkingen te modelleren door de async
en await
trefwoorden te ondersteunen. In de meeste gevallen is het model vrij eenvoudig voor zowel I/O-gebonden als CPU-gebonden scenario's. Binnen een async
methode:
-
I/O-gebonden code een bewerking start die wordt vertegenwoordigd door een
Task
- ofTask<T>
-object binnen de methodeasync
. - CPU-gebonden code start een bewerking op een achtergrondthread met de methode Task.Run.
In beide gevallen vertegenwoordigt een actieve Task
een asynchrone bewerking die mogelijk niet is voltooid.
Het await
trefwoord is waar de magie plaatsvindt. Het draagt de controle over aan de aanroeper van de methode die de await
-expressie bevat, en zorgt er uiteindelijk voor dat de gebruikersinterface responsief is of dat een service dynamisch is. Hoewel er manieren zijn om asynchrone code te benaderen dan met behulp van de async
- en await
-expressies, is dit artikel gericht op de constructies op taalniveau.
Notitie
In sommige voorbeelden in dit artikel wordt de System.Net.Http.HttpClient-klasse gebruikt om gegevens van een webservice te downloaden. In de voorbeeldcode is het s_httpClient
object een statisch veld van het type Program
klasse:
private static readonly HttpClient s_httpClient = new();
Zie de volledige voorbeeldcode aan het einde van dit artikel voor meer informatie.
Onderliggende concepten bekijken
Wanneer u asynchrone programmering implementeert in uw C#-code, transformeert de compiler uw programma in een statusmachine. Met deze constructie worden verschillende bewerkingen en statussen in uw code bijgehouden, zoals het uitvoeren wanneer de code een await
-expressie bereikt en de uitvoering hervat wanneer een achtergrondtaak is voltooid.
In termen van computerwetenschappentheorie is asynchrone programmering een implementatie van het Promise-model van asynchrone.
In het asynchrone programmeermodel zijn er verschillende belangrijke concepten die u moet begrijpen:
- U kunt asynchrone code gebruiken voor zowel I/O-gebonden als CPU-gebonden code, maar de implementatie is anders.
- Asynchrone code maakt gebruik van
Task<T>
enTask
objecten als constructies om werk te modelleren dat op de achtergrond wordt uitgevoerd. - Het
async
trefwoord declareert een methode als asynchrone methode, waarmee u hetawait
trefwoord in de hoofdtekst van de methode kunt gebruiken. - Wanneer u het trefwoord
await
toepast, wordt de aanroepmethode onderbroken en wordt de besturing teruggezet naar de aanroeper totdat de taak is voltooid. - U kunt de
await
expressie alleen gebruiken in een asynchrone methode.
I/O-gebonden voorbeeld: Gegevens downloaden van webservice
Wanneer de gebruiker in dit voorbeeld een knop selecteert, downloadt de app gegevens uit een webservice. U wilt de UI-thread voor de app niet blokkeren tijdens het downloadproces. Met de volgende code wordt deze taak uitgevoerd:
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);
};
Met de code wordt de intentie (het downloaden van gegevens asynchroon) weergegeven zonder dat er sprake is van interactie met Task
objecten.
CPU-gebonden voorbeeld: Gameberekening uitvoeren
In het volgende voorbeeld brengt een mobiel spel schade toe aan verschillende personages op het scherm als reactie op een knopactie. Het uitvoeren van de schadeberekening kan duur zijn. Het uitvoeren van de berekening op de UI-thread kan leiden tot weergave- en ui-interactieproblemen tijdens de berekening.
De beste manier om de taak af te handelen, is door een achtergrondthread te starten om het werk met de Task.Run
-methode te voltooien. De bewerking levert resultaten op door middel van een await
-expressie. De bewerking wordt hervat wanneer de taak is voltooid. Met deze benadering kan de gebruikersinterface soepel worden uitgevoerd terwijl het werk op de achtergrond wordt voltooid.
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);
};
De code geeft duidelijk de intentie van het knop Clicked
-gebeurtenis weer. Hiervoor hoeft u geen achtergrondthread handmatig te beheren en wordt de taak op een niet-blokkerende manier voltooid.
Cpu-gebonden en I/O-gebonden scenario's herkennen
In de vorige voorbeelden ziet u hoe u de async
modifier en await
expressie gebruikt voor I/O-gebonden en CPU-gebonden werk. Een voorbeeld voor elk scenario laat zien hoe de code verschilt op basis van waar de bewerking is gebonden. Om u voor te bereiden op uw implementatie, moet u weten hoe u kunt bepalen wanneer een bewerking I/O-gebonden of CPU-gebonden is. Uw implementatiekeuze kan sterk van invloed zijn op de prestaties van uw code en kan leiden tot onjuiste constructies.
Er zijn twee primaire vragen die u moet oplossen voordat u code schrijft:
Vraag | Scenario | Implementatie |
---|---|---|
Moet de code wachten op een resultaat of actie, zoals gegevens uit een database? | I/O-gebonden | Gebruik de async modifier en de await uitdrukking zonder de Task.Run methode. Vermijd het gebruik van de taakparallelbibliotheek. |
Moet de code een dure berekening uitvoeren? | CPU-gebonden | Gebruik de async -modifier en await -expressie, maar verdeel het werk naar een andere thread met de Task.Run -methode. Met deze aanpak worden problemen met cpu-reactiesnelheid opgelost. Als het werk geschikt is voor gelijktijdigheid en parallelle uitvoering, kunt u ook overwegen om de taakparallelbibliotheek te gebruiken. |
Meet altijd de uitvoering van uw code. Mogelijk ontdekt u dat uw CPU-gebonden werk niet kostbaar genoeg is in vergelijking met de overhead van contextswitches bij het gebruik van multithreading. Elke keuze heeft compromissen. Kies de juiste afweging voor uw situatie.
Andere voorbeelden verken
De voorbeelden in deze sectie laten verschillende manieren zien waarop u asynchrone code kunt schrijven in C#. Ze hebben betrekking op een aantal scenario's die u kunt tegenkomen.
Gegevens extraheren uit een netwerk
Met de volgende code wordt HTML gedownload van een bepaalde URL en wordt het aantal keren geteld dat de tekenreeks .NET voorkomt in de HTML. De code maakt gebruik van ASP.NET om een web-API-controllermethode te definiëren, waarmee de taak wordt uitgevoerd en het aantal wordt geretourneerd.
Notitie
Als u van plan bent HTML-parsering uit te voeren in productiecode, gebruikt u geen reguliere expressies. Gebruik in plaats daarvan een parseerbibliotheek.
[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;
}
U kunt vergelijkbare code schrijven voor een Universele Windows-app en de teltaak uitvoeren na een druk op de knop:
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;
}
Wacht totdat meerdere taken zijn voltooid
In sommige scenario's moet de code meerdere stukjes gegevens gelijktijdig ophalen. De Task
API's bieden methoden waarmee u asynchrone code kunt schrijven waarmee een niet-blokkerende wachttijd op meerdere achtergrondtaken wordt uitgevoerd:
- methode Task.WhenAll
- methode Task.WhenAny
In het volgende voorbeeld ziet u hoe u User
objectgegevens voor een set userId
objecten kunt ophalen.
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);
}
U kunt deze code beknopter schrijven met behulp van LINQ:
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Hoewel u minder code schrijft met behulp van LINQ, moet u voorzichtig zijn bij het combineren van LINQ met asynchrone code. LINQ maakt gebruik van uitgestelde (of luie) uitvoering. Asynchrone aanroepen worden niet onmiddellijk uitgevoerd zoals in een foreach
lus, tenzij u de gegenereerde volgorde dwingt om een aanroep naar de .ToList()
of .ToArray()
methode te herhalen. In dit voorbeeld wordt de methode Enumerable.ToArray gebruikt om de query gretig uit te voeren en de resultaten op te slaan in een matrix. Deze methode dwingt de id => GetUserAsync(id)
instructie uit te voeren en de taak te starten.
Overwegingen voor asynchrone programmering bekijken
Met asynchrone programmering zijn er verschillende details waarmee u rekening moet houden dat onverwacht gedrag kan voorkomen.
Gebruik 'await' binnen de method body van 'async()'
Wanneer u de async
modifier gebruikt, moet u een of meer await
expressies opnemen in de hoofdtekst van de methode. Als de compiler geen await
expressie tegenkomt, kan de methode niet opleveren. Hoewel de compiler een waarschuwing genereert, wordt de code nog steeds gecompileerd en wordt de methode uitgevoerd. De statusmachine die door de C#-compiler voor de asynchrone methode wordt gegenereerd, doet niets, dus het hele proces is zeer inefficiënt.
Voeg het achtervoegsel Async toe aan asynchrone methodenamen
De .NET-stijlconventie is om het achtervoegsel 'Async' toe te voegen aan alle asynchrone methodenamen. Deze aanpak helpt om gemakkelijker onderscheid te maken tussen synchrone en asynchrone methoden. Bepaalde methoden die niet expliciet worden aangeroepen door uw code (zoals gebeurtenishandlers of webcontrollermethoden) zijn niet noodzakelijkerwijs van toepassing in dit scenario. Omdat deze items niet expliciet worden aangeroepen door uw code, is het gebruik van expliciete naamgeving niet zo belangrijk.
Geef 'async void' alleen terug vanuit evenementhandlers
Gebeurtenis-handlers moeten void
retourtypen declareren en kunnen geen Task
en Task<T>
objecten gebruiken of retourneren zoals andere methoden. Wanneer u asynchrone gebeurtenis-handlers schrijft, moet u de async
modifier gebruiken op een void
retourmethode voor de handlers. Andere implementaties van async void
retourmethoden volgen het TAP-model niet en kunnen uitdagingen opleveren:
- Uitzonderingen die in een
async void
-methode worden gegooid, kunnen niet buiten die methode worden afgevangen -
async void
methoden zijn moeilijk te testen -
async void
methoden kunnen negatieve bijwerkingen veroorzaken als de aanroeper niet verwacht dat ze asynchroon zijn
Wees voorzichtig met asynchrone lambdas in LINQ
Het is belangrijk om voorzichtig te zijn bij het implementeren van asynchrone lambdas in LINQ-expressies. Lambda-expressies in LINQ maken gebruik van uitgestelde uitvoering, wat betekent dat de code op een onverwacht moment kan worden uitgevoerd. De introductie van blokkeringstaken in dit scenario kan eenvoudig leiden tot een impasse, als de code niet correct is geschreven. Bovendien kan het nesten van asynchrone code het lastig maken om te redeneren over de uitvoering van de code. Async en LINQ zijn krachtig, maar deze technieken moeten zo zorgvuldig en duidelijk mogelijk samen worden gebruikt.
Geef voorrang aan opdrachten zonder te blokkeren
Als uw programma het resultaat van een taak nodig heeft, schrijft u code die de await
-expressie op een niet-blokkerende manier implementeert. Het blokkeren van de huidige thread als een manier om synchroon te wachten tot een Task
item is voltooid, kan leiden tot impasses en geblokkeerde contextthreads. Deze programmeerbenadering kan complexere foutafhandeling vereisen. De volgende tabel biedt richtlijnen voor het verkrijgen van toegangsresultaten van taken zonder blokkeren:
Taakscenario | Huidige code | Vervangen door 'await' |
---|---|---|
het resultaat van een achtergrondtaak ophalen |
Task.Wait of Task.Result |
await |
Doorgaan wanneer een taak is voltooid | Task.WaitAny |
await Task.WhenAny |
Doorgaan wanneer alle taken zijn voltooid | Task.WaitAll |
await Task.WhenAll |
na enige tijd doorgaan | Thread.Sleep |
await Task.Delay |
Overweeg het gebruik van het type ValueTask
Wanneer een asynchrone methode een Task
-object retourneert, kunnen prestatieknelpunten in bepaalde paden worden geïntroduceerd. Omdat Task
een verwijzingstype is, wordt een Task
-object toegewezen vanuit de heap. Als een methode die is gedeclareerd met de async
modifier een resultaat in de cache retourneert of synchroon wordt voltooid, kunnen de extra toewijzingen aanzienlijke tijdskosten opleveren in prestatiekritieke secties van code. Dit scenario kan kostbaar worden wanneer de toewijzingen plaatsvinden in nauwe lussen. Zie gegeneraliseerde asynchrone retourtypen voor meer informatie.
Begrijpen wanneer u ConfigureAwait(false) instelt
Ontwikkelaars vragen vaak wanneer ze de Task.ConfigureAwait(Boolean) booleaanse waarde moeten gebruiken. Met deze API kan een Task
-exemplaar de context configureren voor de statuscomputer waarmee elke await
-expressie wordt geïmplementeerd. Wanneer de Booleaanse waarde niet juist is ingesteld, kunnen de prestaties afnemen of impasses optreden. Zie ConfigureAwait FAQvoor meer informatie.
Minder stateful code schrijven
Vermijd het schrijven van code die afhankelijk is van de status van globale objecten of de uitvoering van bepaalde methoden. In plaats daarvan is dit alleen afhankelijk van de retourwaarden van methoden. Er zijn veel voordelen voor het schrijven van code die minder stateful is:
- Eenvoudiger om te redeneren over code
- Eenvoudiger om code te testen
- Eenvoudiger om asynchrone en synchrone code te combineren
- In staat om racevoorwaarden in code te voorkomen
- Eenvoudig om asynchrone code te coördineren die afhankelijk is van retourwaarden
- (Bonus) Werkt goed met afhankelijkheidsinjectie in code
Een aanbevolen doel is om volledige of bijna volledige referentiële transparantie in uw code te bereiken. Deze benadering resulteert in een voorspelbare, testbare en onderhoudbare codebasis.
Bekijk het volledige voorbeeld
De volgende code vertegenwoordigt het volledige voorbeeld, dat beschikbaar is in het Program.cs voorbeeldbestand.
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.