Une différence majeure réside dans la propagation des exceptions. Une exception, jetée à l' intérieur d' une async Task
méthode, est stockée dans le retour Task
objet et reste en sommeil jusqu'à ce que la tâche s'observe via await task
, task.Wait()
, task.Result
ou task.GetAwaiter().GetResult()
. Il est propagé de cette manière même s'il est jeté de la partie synchrone de la async
méthode.
Considérez le code suivant, où OneTestAsync
et AnotherTestAsync
se comportent assez différemment:
static async Task OneTestAsync(int n)
{
await Task.Delay(n);
}
static Task AnotherTestAsync(int n)
{
return Task.Delay(n);
}
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
Task task = null;
try
{
task = whatTest(n);
Console.Write("Press enter to continue");
Console.ReadLine();
task.Wait();
}
catch (Exception ex)
{
Console.Write("Error: " + ex.Message);
}
}
Si j'appelle DoTestAsync(OneTestAsync, -2)
, il produit la sortie suivante:
Appuyez sur Entrée pour continuer
Erreur: une ou plusieurs erreurs se sont produites.
Erreur: 2ème
Remarque, j'ai dû appuyer Enterpour le voir.
Maintenant, si j'appelle DoTestAsync(AnotherTestAsync, -2)
, le flux de travail du code à l'intérieur DoTestAsync
est assez différent, tout comme la sortie. Cette fois, on ne m'a pas demandé d'appuyer sur Enter:
Erreur: la valeur doit être -1 (signifiant un délai d'attente infini), 0 ou un entier positif.
Nom du paramètre: millisecondesDelayError: 1st
Dans les deux cas Task.Delay(-2)
jette au début, tout en validant ses paramètres. Cela peut être un scénario inventé, mais en théorie, cela Task.Delay(1000)
peut aussi être déclenché, par exemple, lorsque l'API du minuteur système sous-jacent échoue.
Par ailleurs, la logique de propagation des erreurs est encore différente pour les async void
méthodes (par opposition aux async Task
méthodes). Une exception levée à l'intérieur d'une async void
méthode sera immédiatement relancée sur le contexte de synchronisation du thread courant (via SynchronizationContext.Post
), si le thread actuel en a un ( SynchronizationContext.Current != null)
. Sinon, elle sera relancée via ThreadPool.QueueUserWorkItem
). L'appelant n'a pas la possibilité de gérer cette exception sur le même cadre de pile.
J'ai publié quelques détails supplémentaires sur le comportement de gestion des exceptions TPL ici et ici .
Q : Est-il possible d'imiter le comportement de propagation des exceptions des async
méthodes pour les méthodes non asynchrones Task
, de sorte que ces dernières ne lancent pas sur le même cadre de pile?
R : Si vraiment nécessaire, alors oui, il y a une astuce pour cela:
async Task<int> MethodAsync(int arg)
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
}
Task<int> MethodAsync(int arg)
{
var task = new Task<int>(() =>
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
});
task.RunSynchronously(TaskScheduler.Default);
return task;
}
Notez cependant que dans certaines conditions (comme lorsqu'il est trop profond sur la pile), il RunSynchronously
peut toujours s'exécuter de manière asynchrone.
Une autre différence notable est que
la version async
/ await
est plus sujette au blocage dans un contexte de synchronisation autre que celui par défaut . Par exemple, ce qui suit sera verrouillé dans une application WinForms ou WPF:
static async Task TestAsync()
{
await Task.Delay(1000);
}
void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait();
}
Changez-le en une version non asynchrone et il ne sera pas verrouillé:
Task TestAsync()
{
return Task.Delay(1000);
}
La nature de l'impasse est bien expliquée par Stephen Cleary dans son blog .
await
/async
du tout :)