C# Language
Async-Await
Recherche…
Introduction
En C #, une méthode déclarée async
ne sera pas bloquée dans un processus synchrone, dans le cas où vous utiliseriez des opérations basées sur les E / S (par exemple, accès Web, utilisation de fichiers, ...). Le résultat de telles méthodes marquées par async peut être attendu via l'utilisation du mot-clé await
.
Remarques
Une méthode async
peut renvoyer void
, Task
ou Task<T>
.
Le type de retour Task
attendra la fin de la méthode et le résultat sera void
. Task<T>
renverra une valeur du type T
fois la méthode terminée.
async
méthodes async
doivent renvoyer Task
ou Task<T>
, par opposition à void
, dans presque toutes les circonstances. async void
méthodes async void
ne peuvent pas être await
, ce qui entraîne divers problèmes. Le seul scénario où un async
doit retourner void
est dans le cas d'un gestionnaire d'événements.
async
/ await
fonctionne en transformant votre méthode async
en une machine à états. Pour ce faire, il crée une structure en arrière-plan qui stocke l'état actuel et tout contexte (comme les variables locales) et expose une méthode MoveNext()
pour avancer les états (et exécuter tout code associé) chaque fois qu'une attente attendue se termine.
Appels consécutifs simples
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);
}
La principale chose à noter ici est que si tous les await
méthode -ed est appelé de manière asynchrone - et pour le moment de cet appel le contrôle est donné au système - l'écoulement à l' intérieur de la méthode est linéaire et ne nécessite aucun traitement spécial en raison de asynchronie. Si l'une des méthodes appelées échoue, l'exception sera traitée "comme prévu", ce qui dans ce cas signifie que l'exécution de la méthode sera abandonnée et que l'exception passera à la pile.
Try / Catch / Finalement
A partir de C # 6.0, le mot-clé await
peut maintenant être utilisé dans un bloc catch
et finally
.
try {
var client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
await client.LogExceptionAsync();
throw;
} finally {
await client.CloseAsync();
}
Avant C # 6.0, vous deviez faire quelque chose comme suit. Notez que 6.0 a également nettoyé les vérifications NULL avec l' opérateur Null Propagating .
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;
}
Notez que si vous attendez une tâche non créée par async
(par exemple, une tâche créée par Task.Run
), certains débogueurs peuvent casser des exceptions lancées par la tâche, même si elle est apparemment gérée par la méthode try / catch environnante. Cela se produit car le débogueur considère qu'il n'est pas géré par rapport au code utilisateur. Dans Visual Studio, il existe une option appelée "Just My Code" , qui peut être désactivée pour empêcher le débogueur de se briser dans de telles situations.
Configuration de Web.config sur la cible 4.5 pour un comportement asynchrone correct.
Web.config system.web.httpRuntime doit cibler la version 4.5 pour garantir que le thread loue le contexte de la requête avant de reprendre votre méthode asynchrone.
<httpRuntime targetFramework="4.5" />
Async et wait ont un comportement indéfini sur ASP.NET avant 4.5. L'async / wait reprendra sur un thread arbitraire qui pourrait ne pas avoir le contexte de la requête. Les applications sous charge échoueront aléatoirement avec des exceptions de référence NULL accédant au HttpContext après l'attente. Utiliser HttpContext.Current dans WebApi est dangereux à cause de l’async
Appels simultanés
Il est possible d'attendre plusieurs appels en même temps qu'en invoquant d' abord les tâches awaitable et qui les attend.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await firstTask;
await secondTask;
}
Vous pouvez également utiliser Task.WhenAll
pour regrouper plusieurs tâches en une seule Task
, qui se termine lorsque toutes les tâches réussies sont terminées.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await Task.WhenAll(firstTask, secondTask);
}
Vous pouvez également faire cela dans une boucle, par exemple:
List<Task> tasks = new List<Task>();
while (something) {
// do stuff
Task someAsyncTask = someAsyncMethod();
tasks.Add(someAsyncTask);
}
await Task.WhenAll(tasks);
Pour obtenir les résultats d'une tâche après avoir attendu plusieurs tâches avec Task.WhenAll, attendez simplement la tâche à nouveau. Comme la tâche est déjà terminée, le résultat sera renvoyé
var task1 = SomeOpAsync();
var task2 = SomeOtherOpAsync();
await Task.WhenAll(task1, task2);
var result = await task2;
En outre, Task.WhenAny
peut être utilisé pour exécuter plusieurs tâches en parallèle, telles que Task.WhenAll
ci-dessus, à la différence que cette méthode se terminera lorsque l' une des tâches fournies sera terminée.
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);
}
La Task
renvoyée par RunConcurrentTasksWhenAny
se terminera à la fin de firstTask
, secondTask
ou thirdTask
.
Attendez l'opérateur et le mot-clé asynchrone
await
opérateur et le mot-clé async
réunis:
La méthode asynchrone dans laquelle wait est utilisée doit être modifiée par le mot-clé async .
Le contraire n'est pas toujours vrai: vous pouvez marquer une méthode comme async
sans utiliser await
dans son corps.
Ce qui await
réalité, c'est de suspendre l'exécution du code jusqu'à la fin de la tâche attendue; toute tâche peut être attendue.
Remarque: vous ne pouvez pas attendre la méthode asynchrone qui ne retourne rien (void).
En fait, le mot «suspend» est un peu trompeur car non seulement l'exécution s'arrête, mais le thread peut devenir libre pour exécuter d'autres opérations. Sous le capot, await
est implémenté par un peu de magie du compilateur: il divise une méthode en deux parties - avant et après l' await
. La dernière partie est exécutée à la fin de la tâche attendue.
Si nous ignorons certains détails importants, le compilateur le fait grosso modo pour vous:
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;
}
devient:
public Task<TResult> DoIt()
{
// ...
return someTask.ContinueWith(task => {
var result = ((Task<TSomeResult>)task).Result;
return DoIt_Continuation(result);
});
}
private TResult DoIt_Continuation(TSomeResult awaitedResult)
{
// ...
}
Toute méthode habituelle peut être transformée en asynchrone de la manière suivante:
await Task.Run(() => YourSyncMethod());
Cela peut être avantageux lorsque vous devez exécuter une méthode longue sur le thread d'interface utilisateur sans bloquer l'interface utilisateur.
Mais il y a une remarque très importante ici: Asynchrone ne signifie pas toujours concurrente (parallèle ou même multithread). Même sur un seul thread, async
- await
permet toujours le code asynchrone. Par exemple, consultez ce planificateur de tâches personnalisé. Un tel planificateur de tâches «fou» peut simplement transformer des tâches en fonctions appelées dans le traitement de la boucle de messages.
Nous devons nous demander: quel thread exécutera la suite de notre méthode DoIt_Continuation
?
Par défaut, l'opérateur d' await
planifie l'exécution de la continuation avec le contexte de synchronisation actuel . Cela signifie que par défaut pour WinForms et la continuation WPF s'exécute dans le thread d'interface utilisateur. Si, pour une raison quelconque, vous devez modifier ce comportement, utilisez la méthode Task.ConfigureAwait()
:
await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);
Retourner une tâche sans attendre
Les méthodes qui effectuent des opérations asynchrones ne ont pas besoin d'utiliser await
si:
- Il n'y a qu'un seul appel asynchrone dans la méthode
- L'appel asynchrone est à la fin de la méthode
- Une exception de capture / traitement pouvant survenir dans la tâche n'est pas nécessaire
Considérez cette méthode qui renvoie une Task
:
public async Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return await dataStore.GetByKeyAsync(lookupKey);
}
Si GetByKeyAsync
a la même signature que GetUserAsync
(en retournant une Task<User>
), la méthode peut être simplifiée:
public Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return dataStore.GetByKeyAsync(lookupKey);
}
Dans ce cas, la méthode n'a pas besoin d'être marquée async
, même si elle effectue une opération asynchrone. Le Groupe retourné par GetByKeyAsync
est transmis directement à la méthode d'appel, où il sera await
ed.
Important : Le fait de renvoyer la Task
au lieu de l'attendre modifie le comportement d'exception de la méthode, car elle ne déclenche pas l'exception dans la méthode qui lance la tâche mais dans la méthode qui l'attend.
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();
Cela améliorera les performances car cela permettra au compilateur d'économiser la génération d'une machine à états asynchrone supplémentaire.
Le blocage sur du code asynchrone peut provoquer des blocages
Il est déconseillé de bloquer les appels asynchrones car cela peut provoquer des blocages dans des environnements dotés d'un contexte de synchronisation. La meilleure pratique consiste à utiliser Async / Wait "tout en bas". Par exemple, le code Windows Forms suivant provoque un blocage:
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");
}
Essentiellement, une fois l'appel asynchrone terminé, il attend que le contexte de synchronisation devienne disponible. Cependant, le gestionnaire d'événement "conserve" le contexte de synchronisation pendant qu'il attend que la méthode TryThis()
se termine, provoquant ainsi une attente circulaire.
Pour corriger cela, le code doit être modifié pour
private async void button1_Click(object sender, EventArgs e)
{
bool result = await TryThis();
Trace.TraceInformation("Done with result");
}
Remarque: les gestionnaires d'événements sont le seul endroit où async void
doit être utilisé (car vous ne pouvez pas attendre une méthode async void
).
Async / wait n'améliorera les performances que si elle permet à la machine d'effectuer des tâches supplémentaires
Considérez le code suivant:
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);
}
Cela ne fonctionnera pas mieux que
public void MethodA()
{
MethodB();
// Do other work
}
public void MethodB()
{
MethodC();
// Do other work
}
public void MethodC()
{
Thread.Sleep(100);
}
Le but principal de async / waiting est de permettre à la machine d'effectuer un travail supplémentaire, par exemple pour permettre au thread appelant d'effectuer d'autres tâches en attendant le résultat d'une opération d'E / S. Dans ce cas, le thread appelant n'est jamais autorisé à faire plus de travail que ce qu'il aurait pu faire autrement, il n'y a donc pas de gain de performance par rapport à l'appel MethodA()
, MethodB()
et MethodC()
.