C# Language
Async-Invänta
Sök…
Introduktion
I C # kommer en metod som förklaras async
inte att blockeras inom en synkron process, om du använder I / O-baserade operationer (t.ex. webbåtkomst, arbetar med filer, ...). Resultatet av sådana async-markerade metoder kan await
användning av nyckelordet som await
.
Anmärkningar
En async
metod kan returnera void
, Task
eller Task<T>
.
Returtypens Task
väntar på att metoden ska slutföras och resultatet blir void
. Task<T>
kommer att returnera ett värde från typ T
efter att metoden är klar.
async
bör returnera Task
eller Task<T>
, i motsats till void
, under nästan alla omständigheter. async void
metoder kan inte await
, vilket leder till en mängd olika problem. Det enda scenariet där en async
ska återgå till void
är i fallet med en händelsehanterare.
async
/ await
fungerar genom att omvandla din async
metod till en tillståndsmaskin. Det gör detta genom att skapa en struktur bakom kulisserna som lagrar det aktuella tillståndet och alla sammanhang (som lokala variabler), och exponerar en MoveNext()
-metod för att förflytta tillstånd (och köra en tillhörande kod) när en väntad väntan är klar.
Enkla samtal i följd
public async Task<JobResult> GetDataFromWebAsync()
{
var nextJob = await _database.GetNextJobAsync();
var response = await _httpClient.GetAsync(nextJob.Uri);
var pageContents = await response.Content.ReadAsStringAsync();
return await _database.SaveJobResultAsync(pageContents);
}
Det viktigaste att notera här är att medan varje await
-ED metod kallas asynkront - och under tiden för det samtalet kontrollen gav tillbaka till systemet - flödet inne i metoden är linjär och inte kräver någon speciell behandling på grund av asynkronism. Om någon av de metoder som kallas misslyckas behandlas undantaget "som förväntat", vilket i detta fall innebär att metodutförandet avbryts och undantaget kommer att gå upp i bunten.
Try / Catch / Finally
Från och med C # 6.0 kan nyckelordet nu await
en catch
och finally
blockeras.
try {
var client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
await client.LogExceptionAsync();
throw;
} finally {
await client.CloseAsync();
}
Innan C # 6.0 skulle du behöva göra något enligt följande. Observera att 6.0 också rensat upp nollkontrollerna med Null Propagating-operatören .
AsynClient client;
MyException caughtException;
try {
client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
caughtException = ex;
}
if (client != null) {
if (caughtException != null) {
await client.LogExceptionAsync();
}
await client.CloseAsync();
if (caughtException != null) throw caughtException;
}
Observera att om du väntar på en uppgift som inte har skapats av async
(t.ex. en uppgift som skapats av Task.Run
), kan vissa felsökare bryta på undantag som kastas av uppgiften även om den till synes hanteras av den omgivande försök / fången. Detta händer eftersom felsökaren anser att den inte kan hanteras med avseende på användarkod. I Visual Studio finns det ett alternativ som heter "Just My Code" , som kan inaktiveras för att förhindra att felsökaren bryter i sådana situationer.
Web.config-inställning till mål 4.5 för korrekt async-beteende.
Web.config-systemet.web.httpRuntime måste inriktas på 4.5 för att säkerställa att tråden kommer att hålla begäran sammanhanget innan du fortsätter din async-metod.
<httpRuntime targetFramework="4.5" />
Async och inväntar har odefinierat beteende på ASP.NET före 4.5. Async / vänta kommer att återupptas på en godtycklig tråd som kanske inte har förfrågningskontext. Program under last misslyckas slumpmässigt med undantag från nollreferenser som går åt HttpContext efter väntan. Att använda HttpContext.Current i WebApi är farligt på grund av async
Samtidiga samtal
Det är möjligt att vänta på flera samtal samtidigt genom att först åberopa de väntade uppgifterna och sedan vänta på dem.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await firstTask;
await secondTask;
}
Alternativt kan Task.WhenAll
användas för att gruppera flera uppgifter i en enda Task
, som slutförs när alla dess godkända uppgifter är slutförda.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await Task.WhenAll(firstTask, secondTask);
}
Du kan också göra detta i en slinga, till exempel:
List<Task> tasks = new List<Task>();
while (something) {
// do stuff
Task someAsyncTask = someAsyncMethod();
tasks.Add(someAsyncTask);
}
await Task.WhenAll(tasks);
För att få resultat från en uppgift efter att ha väntat på flera uppgifter med Task.WhenAll, vänta helt enkelt på uppgiften igen. Eftersom uppgiften redan är klar kommer den bara att returnera resultatet
var task1 = SomeOpAsync();
var task2 = SomeOtherOpAsync();
await Task.WhenAll(task1, task2);
var result = await task2;
Även Task.WhenAny
kan användas för att utföra flera uppgifter parallellt, som Task.WhenAll
ovan, med skillnaden att den här metoden kommer att slutföras när någon av de medföljande uppgifterna kommer att slutföras.
public async Task RunConcurrentTasksWhenAny()
{
var firstTask = TaskOperation("#firstTask executed");
var secondTask = TaskOperation("#secondTask executed");
var thirdTask = TaskOperation("#thirdTask executed");
await Task.WhenAny(firstTask, secondTask, thirdTask);
}
Task
returneras av RunConcurrentTasksWhenAny
kommer att slutföras när någon av första firstTask
, andra secondTask
eller thirdTask
klar.
Vänta på operatörs- och async-nyckelord
await
operatör och async
nyckelord samman:
Den asynkrona metoden där väntan används måste modifieras av sökordet async .
Det motsatta är inte alltid sant: du kan markera en metod som async
utan att använda await
i kroppen.
Det som await
faktiskt är att avbryta körningen av koden tills den väntade uppgiften är klar. alla uppgifter kan vänta.
Obs! Du kan inte vänta på asynkmetod som returnerar ingenting (ogiltigt).
Egentligen är ordet "avstängd" lite vilseledande eftersom inte bara exekveringen stoppar, utan tråden kan bli fri för att utföra andra operationer. Under huven implementeras await
med lite kompilermagi: den delar upp en metod i två delar - före och efter await
. Den senare delen körs när den väntade uppgiften är klar.
Om vi ignorerar några viktiga detaljer, gör kompilatorn ungefär detta åt dig:
public async Task<TResult> DoIt()
{
// do something and acquire someTask of type Task<TSomeResult>
var awaitedResult = await someTask;
// ... do something more and produce result of type TResult
return result;
}
blir:
public Task<TResult> DoIt()
{
// ...
return someTask.ContinueWith(task => {
var result = ((Task<TSomeResult>)task).Result;
return DoIt_Continuation(result);
});
}
private TResult DoIt_Continuation(TSomeResult awaitedResult)
{
// ...
}
Alla vanliga metoder kan förvandlas till async på följande sätt:
await Task.Run(() => YourSyncMethod());
Detta kan vara fördelaktigt när du behöver köra en lång körningsmetod på UI-tråden utan att frysa gränssnittet.
Men det finns en mycket viktig kommentar här: Asynkron betyder inte alltid samtidigt (parallell eller till och med flergängad). Även på en enda tråd async
- await
fortfarande asynkron kod. Se till exempel denna anpassade uppgiftsschemaläggare . En sådan "galen" uppgiftsschemaläggare kan helt enkelt förvandla uppgifter till funktioner som kallas inom behandling av meddelandeslinga.
Vi måste fråga oss själva: Vilken tråd kommer att genomföra fortsättningen av vår metod DoIt_Continuation
?
Som standard schemaläggs den await
operatören exekveringen av fortsättning med det aktuella synkroniseringssammanhanget . Det betyder att WinForms och WPF fortsätter som standard i UI-tråden som standard. Om du av någon anledning behöver ändra detta beteende använder du metoden Task.ConfigureAwait()
:
await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);
Återvända en uppgift utan att vänta
Metoder som utför asynkron operation behöver inte await
om:
- Det finns bara ett asynkront samtal i metoden
- Det asynkrona samtalet är i slutet av metoden
- Fångst / hantering undantag som kan hända inom uppgiften är inte nödvändigt
Tänk på den här metoden som returnerar en Task
:
public async Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return await dataStore.GetByKeyAsync(lookupKey);
}
Om GetByKeyAsync
har samma signatur som GetUserAsync
(returnerar en Task<User>
GetUserAsync
Task<User>
) kan metoden förenklas:
public Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return dataStore.GetByKeyAsync(lookupKey);
}
I det här fallet behöver metoden inte markeras async
, även om den förformar en asynkron operation. Uppgiften som returneras av GetByKeyAsync
överförs direkt till samtalsmetoden, där den kommer att await
.
Viktigt : Återvända Task
istället för att vänta på den, ändrar metodens undantagsbeteende, eftersom det inte kommer att kasta undantaget in i metoden som startar uppgiften utan i metoden som väntar på den.
public Task SaveAsync()
{
try {
return dataStore.SaveChangesAsync();
}
catch(Exception ex)
{
// this will never be called
logger.LogException(ex);
}
}
// Some other code calling SaveAsync()
// If exception happens, it will be thrown here, not inside SaveAsync()
await SaveAsync();
Detta kommer att förbättra prestanda eftersom det sparar kompilatorn genereringen av en extra async- tillståndsmaskin.
Blockering av async-kod kan orsaka dödlås
Det är en dålig praxis att blockera på async-samtal eftersom det kan orsaka dödlås i miljöer som har en synkroniseringskontext. Den bästa praxis är att använda async / vänta "hela vägen ner." Till exempel orsakar följande Windows Forms-kod en dödlås:
private async Task<bool> TryThis()
{
Trace.TraceInformation("Starting TryThis");
await Task.Run(() =>
{
Trace.TraceInformation("In TryThis task");
for (int i = 0; i < 100; i++)
{
// This runs successfully - the loop runs to completion
Trace.TraceInformation("For loop " + i);
System.Threading.Thread.Sleep(10);
}
});
// This never happens due to the deadlock
Trace.TraceInformation("About to return");
return true;
}
// Button click event handler
private void button1_Click(object sender, EventArgs e)
{
// .Result causes this to block on the asynchronous call
bool result = TryThis().Result;
// Never actually gets here
Trace.TraceInformation("Done with result");
}
När async-samtalet slutförs väntar det på att synkroniseringskonteksten blir tillgängligt. Men händelseshanteraren "håller fast" i synkroniseringssituationen medan den väntar på att TryThis()
-metoden ska slutföras och därmed orsakar en cirkulär vänta.
För att fixa detta bör kod ändras till
private async void button1_Click(object sender, EventArgs e)
{
bool result = await TryThis();
Trace.TraceInformation("Done with result");
}
Obs: händelsehanterare är den enda platsen där async void
ska användas (eftersom du inte kan vänta på en async void
metod).
Async / wait kommer bara att förbättra prestanda om det gör det möjligt för maskinen att utföra ytterligare arbete
Tänk på följande kod:
public async Task MethodA()
{
await MethodB();
// Do other work
}
public async Task MethodB()
{
await MethodC();
// Do other work
}
public async Task MethodC()
{
// Or await some other async work
await Task.Delay(100);
}
Detta kommer inte att prestera bättre än
public void MethodA()
{
MethodB();
// Do other work
}
public void MethodB()
{
MethodC();
// Do other work
}
public void MethodC()
{
Thread.Sleep(100);
}
Det primära syftet med async / invänta är att låta maskinen utföra ytterligare arbete - till exempel att låta den ringande tråden göra annat arbete medan den väntar på ett resultat från någon I / O-operation. I det här fallet får den ringande tråden aldrig göra mer arbete än vad det annars skulle ha kunnat göra, så det finns ingen prestationsförstärkning än att bara ringa MethodA()
, MethodB()
och MethodC()
synkront.