Asynkrona programmeringsscenarier
Om du har några I/O-bundna behov (till exempel att begära data från ett nätverk, komma åt en databas eller läsa och skriva till ett filsystem) vill du använda asynkron programmering. Du kan också ha CPU-bunden kod, till exempel att utföra en dyr beräkning, vilket också är ett bra scenario för att skriva asynkron kod.
C# har en asynkron programmeringsmodell på språknivå som gör det enkelt att skriva asynkron kod utan att behöva jonglera motringningar eller följa ett bibliotek som stöder asynkron kod. Den följer det så kallade aktivitetsbaserade asynkrona mönstret (TAP).
Översikt över den asynkrona modellen
Kärnan i asynkron programmering är objekten Task
och Task<T>
som modellerar asynkrona åtgärder. De stöds av nyckelorden async
och await
. Modellen är ganska enkel i de flesta fall:
- För I/O-bunden kod väntar du på en åtgärd som returnerar en
Task
ellerTask<T>
inuti enasync
metod. - För CPU-bunden kod väntar du på en åtgärd som startas i en bakgrundstråd med Task.Run metoden .
Nyckelordet await
är där magin händer. Den ger kontroll till anroparen för den metod som utförde await
, och i slutändan gör det möjligt för ett användargränssnitt att vara dynamiskt eller att en tjänst är elastisk. Även om det finns sätt att använda asynkron kod förutom async
och await
fokuserar den här artikeln på konstruktionerna på språknivå.
Kommentar
I några av följande exempel System.Net.Http.HttpClient används klassen för att ladda ned vissa data från en webbtjänst.
Objektet s_httpClient
som används i dessa exempel är ett statiskt fält i Program
klassen (kontrollera hela exemplet):
private static readonly HttpClient s_httpClient = new();
I/O-bundet exempel: Ladda ned data från en webbtjänst
Du kan behöva ladda ned vissa data från en webbtjänst när en knapp trycks ned men inte vill blockera användargränssnittstråden. Det kan åstadkommas så här:
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: Utföra en beräkning för ett spel
Anta att du skriver ett mobilspel där du kan skada många fiender på skärmen genom att trycka på en knapp. Att utföra skadeberäkningen kan vara dyrt, och om du gör det i användargränssnittstråden skulle spelet se ut att pausa när beräkningen utförs!
Det bästa sättet att hantera detta är att starta en bakgrundstråd, som utför arbetet med och Task.Run
väntar på resultatet med hjälp av await
. Detta gör att användargränssnittet kan kännas smidigt när arbetet utförs.
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);
};
Den här koden uttrycker tydligt avsikten med knappens klickhändelse, den kräver inte att du hanterar en bakgrundstråd manuellt och det gör den på ett icke-blockerande sätt.
Vad händer under täcket
På C#-sidan av saker omvandlar kompilatorn koden till en tillståndsdator som håller reda på saker som att ge körning när en await
nås och återuppta körningen när ett bakgrundsjobb har slutförts.
För den teoretiskt lutande är detta en implementering av promise-modellen för asynkron.
Viktiga delar att förstå
- Asynkron kod kan användas för både I/O-bunden och CPU-bunden kod, men olika för varje scenario.
- Asynkron kod använder
Task<T>
ochTask
, som är konstruktioner som används för att modellera arbete som utförs i bakgrunden. - Nyckelordet
async
omvandlar en metod till en asynkron metod, vilket gör att du kan använda nyckelordetawait
i dess brödtext. - När nyckelordet
await
tillämpas pausar det anropande metoden och ger kontroll tillbaka till anroparen tills den väntade aktiviteten har slutförts. await
kan endast användas i en asynkron metod.
Identifiera processorbundet och I/O-bundet arbete
De första två exemplen i den här guiden visade hur du kan använda async
och await
för I/O-bundet och CPU-bundet arbete. Det är viktigt att du kan identifiera när ett jobb du behöver göra är I/O-bunden eller CPU-bunden eftersom det kan påverka kodens prestanda avsevärt och potentiellt kan leda till att vissa konstruktioner missbrukas.
Här är två frågor du bör ställa innan du skriver någon kod:
Kommer koden att "vänta" på något, till exempel data från en databas?
Om svaret är "ja" är ditt arbete I/O-bundet.
Kommer koden att utföra en dyr beräkning?
Om du svarade "ja" är ditt arbete CPU-bundet.
Om det arbete du har är I/O-bundet använder async
du och await
utan Task.Run
. Du bör inte använda det parallella aktivitetsbiblioteket.
Om det arbete du har är CPU-bundet och du bryr dig om svarstider använder async
du och await
, men skapar arbetet på en annan tråd med Task.Run
. Om arbetet är lämpligt för samtidighet och parallellitet bör du även överväga att använda det parallella aktivitetsbiblioteket.
Dessutom bör du alltid mäta körningen av koden. Du kan till exempel hamna i en situation där ditt CPU-bundna arbete inte är tillräckligt dyrt jämfört med omkostnaderna för kontextväxlar vid multitrådning. Varje val har sin kompromiss, och du bör välja rätt kompromiss för din situation.
Fler exempel
I följande exempel visas olika sätt att skriva asynkron kod i C#. De beskriver några olika scenarier som du kan stöta på.
Extrahera data från ett nätverk
Det här kodfragmentet laddar ned HTML-koden från den angivna URL:en och räknar antalet gånger strängen ".NET" inträffar i HTML-koden. Den använder ASP.NET för att definiera en webb-API-kontrollantmetod, som utför den här uppgiften och returnerar talet.
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;
}
Här är samma scenario skrivet för en Universell Windows-app, som utför samma uppgift när en knapp trycks ned:
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;
}
Vänta tills flera uppgifter har slutförts
Du kan hamna i en situation där du behöver hämta flera datastycken samtidigt. API:et Task
innehåller två metoder och Task.WhenAll Task.WhenAny, som gör att du kan skriva asynkron kod som utför en icke-blockerande väntan på flera bakgrundsjobb.
Det här exemplet visar hur du kan hämta User
data för en uppsättning 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);
}
Här är ett annat sätt att skriva detta mer kortfattat med 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 det är mindre kod bör du vara försiktig när du blandar LINQ med asynkron kod. Eftersom LINQ använder uppskjuten (lat) körning sker asynkrona anrop inte omedelbart som de gör i en foreach
loop om du inte tvingar den genererade sekvensen att iterera med ett anrop till .ToList()
eller .ToArray()
. Exemplet ovan använder Enumerable.ToArray för att utföra frågan ivrigt och lagra resultaten i en matris. Det tvingar koden id => GetUserAsync(id)
att köra och starta uppgiften.
Viktig information och råd
Med asynkron programmering finns det vissa detaljer att tänka på som kan förhindra oväntat beteende.
async
metoder måste ha enawait
nyckelord i kroppen eller de kommer aldrig att ge!Detta är viktigt att tänka på. Om
await
den inte används i en metods brödtextasync
genererar C#-kompilatorn en varning, men koden kompileras och körs som om det vore en normal metod. Detta är otroligt ineffektivt eftersom tillståndsdatorn som genereras av C#-kompilatorn för async-metoden inte åstadkommer någonting.Lägg till "Async" som suffix för varje asynkront metodnamn som du skriver.
Det här är den konvention som används i .NET för att enklare skilja synkrona och asynkrona metoder åt. Vissa metoder som inte uttryckligen anropas av koden (till exempel händelsehanterare eller webbstyrenhetsmetoder) gäller inte nödvändigtvis. Eftersom de inte uttryckligen anropas av din kod är det inte lika viktigt att vara explicit om deras namngivning.
async void
ska endast användas för händelsehanterare.async void
är det enda sättet att tillåta asynkrona händelsehanterare att fungera eftersom händelser inte har returtyper (kan därför inte användaTask
ochTask<T>
). All annan användning avasync void
följer inte TAP-modellen och kan vara svårt att använda, till exempel:- 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 dåliga biverkningar om anroparen inte förväntar sig att de ska vara asynkrona.
- Undantag som genereras i en
Gå försiktigt fram när du använder asynkrona lambdas i LINQ-uttryck
Lambda-uttryck i LINQ använder uppskjuten körning, vilket innebär att kod kan köras vid en tidpunkt då du inte förväntar dig det. Införandet av blockerande uppgifter i detta kan enkelt leda till ett dödläge om det inte skrivs korrekt. Dessutom kan kapsling av asynkron kod som den här också göra det svårare att resonera om körningen av koden. Async och LINQ är kraftfulla men bör användas tillsammans så noggrant och tydligt som möjligt.
Skriv kod som väntar på aktiviteter på ett icke-blockerande sätt
Att blockera den aktuella tråden som ett sätt att vänta tills en
Task
har slutförts kan leda till dödlägen och blockerade kontexttrådar och kan kräva mer komplex felhantering. Följande tabell innehåller vägledning om hur du hanterar väntan på uppgifter på ett icke-blockerande sätt:Använder du det här … I stället för det här... När du vill göra det här... await
Task.Wait
ellerTask.Result
Hämtar resultatet av en bakgrundsaktivitet await Task.WhenAny
Task.WaitAny
Väntar på att en uppgift ska slutföras await Task.WhenAll
Task.WaitAll
Väntar på att alla uppgifter ska slutföras await Task.Delay
Thread.Sleep
Väntar under en tidsperiod Överväg att använda där
ValueTask
det är möjligtOm du returnerar ett
Task
objekt från asynkrona metoder kan prestandaflaskhalsar introduceras i vissa sökvägar.Task
är en referenstyp, så att använda den innebär att allokera ett objekt. Om en metod som deklareras medasync
modifieraren returnerar ett cachelagrat resultat eller slutförs synkront kan de extra allokeringarna bli en betydande tidskostnad i prestandakritiska kodavsnitt. Det kan bli kostsamt om dessa allokeringar sker i snäva loopar. Mer information finns i generaliserade asynkrona returtyper.Överväg att använda
ConfigureAwait(false)
En vanlig fråga är "när ska jag använda Task.ConfigureAwait(Boolean) metoden?". Metoden gör det möjligt för en
Task
instans att konfigurera dess awaiter. Detta är ett viktigt övervägande och att ange det felaktigt kan potentiellt få prestandakonsekvenser och till och med dödlägen. Mer information omConfigureAwait
finns i Vanliga frågor och svar om ConfigureAwait.Skriva mindre tillståndskänslig kod
Beror inte på tillståndet för globala objekt eller körningen av vissa metoder. I stället beror det bara på metodernas returvärden. Varför?
- Kod blir lättare att resonera om.
- Koden blir enklare att testa.
- Det är mycket enklare att blanda asynkron och synkron kod.
- Tävlingsförhållanden kan vanligtvis undvikas helt och hållet.
- Beroende på returvärden är det enkelt att samordna asynkron kod.
- (Bonus) det fungerar riktigt bra med beroendeinmatning.
Ett rekommenderat mål är att uppnå fullständig eller nästan fullständig referenstransparens i koden. Detta resulterar i en förutsägbar, testbar och underhållsbar kodbas.
Fullständigt exempel
Följande kod är den fullständiga texten i Program.cs-filen för exemplet.
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.