Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Async/await simplifie la programmation asynchrone, mais certaines erreurs apparaissent à plusieurs reprises. Cet article décrit les cinq bogues les plus courants dans le code asynchrone et vous montre comment corriger chacun d’eux.
La méthode asynchrone s’exécute de manière synchrone
L’ajout du mot clé async à une méthode ne fait pas exécuter la méthode sur un thread d’arrière-plan. Il indique au compilateur d’autoriser await dans le corps de la méthode et d’encapsuler la valeur de retour dans un Task. Lorsque vous appelez une méthode asynchrone, elle s’exécute de manière synchrone jusqu’à ce qu’elle atteigne la première await sur une awaitable incomplète. Si la méthode ne contient aucune expression await ou si chaque tâche en attente qu’elle attend est déjà terminée, alors la méthode se termine complètement sur le thread appelant :
public static class SyncExecutionExample
{
public static Task<int> ComputeAsync()
{
// No await in this method — it runs entirely synchronously.
return Task.FromResult(42);
}
}
Public Module SyncExecutionExample
Public Function ComputeAsync() As Task(Of Integer)
' No Await in this method — it runs entirely synchronously.
Return Task.FromResult(42)
End Function
End Module
Ici, la méthode renvoie immédiatement une tâche accomplie, car elle ne cède jamais. Le compilateur émet un avertissement lorsqu’une méthode asynchrone ne contient await pas d’expressions.
Si votre objectif est de décharger le travail lié au CPU sur un thread de pool de threads, utilisez Run plutôt que async :
public static class OffloadExample
{
public static int ComputeIntensive()
{
int sum = 0;
for (int i = 0; i < 1_000; i++)
sum += i;
return sum;
}
public static Task<int> ComputeOnThreadPoolAsync()
{
return Task.Run(() => ComputeIntensive());
}
}
Public Module OffloadExample
Public Function ComputeIntensive() As Integer
Dim sum As Integer = 0
For i As Integer = 0 To 999
sum += i
Next
Return sum
End Function
Public Function ComputeOnThreadPoolAsync() As Task(Of Integer)
Return Task.Run(Function() ComputeIntensive())
End Function
End Module
Pour plus d’informations sur l’utilisation Task.Run, consultez wrappers asynchrones pour les méthodes synchrones.
Impossible d’attendre une méthode async void
Lorsque vous convertissez une méthode synchrone de retour void en asynchrone, remplacez le type de retour par Task. Si vous laissez le type de retour comme void, la méthode devient « async void », que vous ne pouvez pas awaiter :
public static class AsyncVoidExample
{
// BAD: async void — can't be awaited.
public static async void DoWorkBadAsync()
{
await Task.Delay(100);
}
// GOOD: async Task — callers can await this.
public static async Task DoWorkGoodAsync()
{
await Task.Delay(100);
}
}
Public Module AsyncVoidExample
' BAD: Async Sub — can't be awaited.
Public Async Sub DoWorkBadAsync()
Await Task.Delay(100)
End Sub
' GOOD: Async Function returning Task — callers can await this.
Public Async Function DoWorkGoodAsync() As Task
Await Task.Delay(100)
End Function
End Module
Les méthodes async void servent un objectif spécifique : les gestionnaires d’événements de niveau supérieur dans les infrastructures d’interface utilisateur. En dehors des gestionnaires d’événements, retournez toujours Task ou Task<T> à partir de méthodes asynchrones. Les méthodes Async void présentent ces inconvénients :
- Les exceptions passent inaperçues. Les exceptions levées dans une méthode async void se propagent à celle SynchronizationContext qui était active au démarrage de la méthode. L’appelant ne peut pas intercepter ces exceptions.
- Les appelants ne peuvent pas suivre l’achèvement. Sans un
Task, il n’y a aucun mécanisme à savoir quand l’opération se termine. - Les tests sont difficiles. Vous ne pouvez pas, dans un test, attendre la méthode pour vérifier son comportement.
Interblocages dus au blocage du code asynchrone
Ce bogue est la cause la plus courante du code asynchrone qui « ne se termine jamais ». Cela se produit lorsque vous bloquez de manière synchrone (appel Wait, Task<TResult>.Result, ou GetAwaiter.GetResult) sur un thread qui a un modèle de SynchronizationContext à thread unique.
Séquence qui provoque un interblocage :
- Le code sur le thread de l'interface utilisateur (ou un thread de requête ASP.NET dans les anciennes versions d'ASP.NET) appelle une méthode asynchrone et bloque sur la tâche retournée.
- La méthode asynchrone attend une tâche incomplète sans utiliser
ConfigureAwait(false). - Une fois la tâche attendue terminée, la continuation tente de revenir à l’original
SynchronizationContext. - Le thread de ce contexte est bloqué en attente de la fin de la tâche : interblocage.
public static class DeadlockExample
{
public static async Task<string> GetDataAsync()
{
// Without ConfigureAwait(false), this continuation
// posts back to the original SynchronizationContext.
await Task.Delay(100);
return "data";
}
public static void CallerThatDeadlocks()
{
// On a single-threaded SynchronizationContext (e.g. UI thread),
// the following line deadlocks because the continuation needs
// the same thread that .Result is blocking.
string result = GetDataAsync().Result;
}
}
Public Module DeadlockExample
Public Async Function GetDataAsync() As Task(Of String)
' Without ConfigureAwait(False), this continuation
' posts back to the original SynchronizationContext.
Await Task.Delay(100)
Return "data"
End Function
Public Sub CallerThatDeadlocks()
' On a single-threaded SynchronizationContext (e.g. UI thread),
' the following line deadlocks because the continuation needs
' the same thread that .Result is blocking.
Dim result As String = GetDataAsync().Result
End Sub
End Module
Comment éviter les interblocages
Utilisez une ou plusieurs de ces stratégies :
Ne bloquez pas. Utilisez
awaitplutôt que.Resultou.Wait().public static class DeadlockFix1 { public static async Task CallerFixedAsync() { // Use await instead of .Result string result = await DeadlockExample.GetDataAsync(); Console.WriteLine(result); } }Public Module DeadlockFix1 Public Async Function CallerFixedAsync() As Task ' Use Await instead of .Result Dim result As String = Await DeadlockExample.GetDataAsync() Console.WriteLine(result) End Function End ModuleUtiliser
ConfigureAwait(false)dans le code de la bibliothèque. Lorsque la méthode de votre bibliothèque n’a pas besoin de reprendre dans le contexte de l’appelant, spécifiezConfigureAwait(false)à chaqueawait:public static class DeadlockFix2 { public static async Task<string> GetDataSafeAsync() { await Task.Delay(100).ConfigureAwait(false); return "data"; } }Public Module DeadlockFix2 Public Async Function GetDataSafeAsync() As Task(Of String) Await Task.Delay(100).ConfigureAwait(False) Return "data" End Function End ModuleL’utilisation de
ConfigureAwait(false)informe l'environnement d'exécution de ne pas transférer la continuation vers l’originalSynchronizationContext. Cette approche protège les appelants qui bloquent et améliore les performances en évitant les commutations de threads inutiles.
Avertissement
Blocages statiques du constructeur. Le CLR contient un verrou lors de l’exécution de constructeurs statiques (cctor). Si un constructeur statique bloque une tâche et que la continuation de cette tâche doit exécuter du code dans le même type (ou un type impliqué dans la chaîne de construction), la continuation ne peut pas continuer, car le cctor verrou est conservé. Évitez de bloquer complètement les appels à l’intérieur des constructeurs statiques.
Dépliage de la<tâche>
Lorsque vous passez une lambda asynchrone à une méthode telle que StartNew, l’objet retourné est un Task<Task> (ou Task<Task<TResult>>), et non un simple Task. La tâche externe se termine dès que l’lambda asynchrone atteint son premier rendement await. Il n’attend pas que la tâche interne se termine :
public static class TaskTaskBugExample
{
public static async Task DemoAsync()
{
var sw = Stopwatch.StartNew();
// StartNew returns Task<Task>, not Task.
// The outer task completes immediately when the lambda yields.
await Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
});
// Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module TaskTaskBugExample
Public Async Function DemoAsync() As Task
Dim sw = Stopwatch.StartNew()
' StartNew returns Task(Of Task), not Task.
' The outer task completes immediately when the lambda yields.
Await Task.Factory.StartNew(Async Function()
Await Task.Delay(1000)
End Function)
' Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
Corrigez ce problème de l’une des trois manières suivantes :
Utilisez Run à la place.
Task.Rundésencapsule automatiquementTask<Task>:public static class TaskTaskFix1 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Run(async () => { await Task.Delay(1000); }); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix1 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Run(Async Function() Await Task.Delay(1000) End Function) Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End ModuleAppeler Unwrap sur le résultat :
public static class TaskTaskFix2 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Factory.StartNew(async () => { await Task.Delay(1000); }).Unwrap(); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix2 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function).Unwrap() Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End ModuleAttendez deux fois (d’abord la tâche externe, puis l’intérieur) :
public static class TaskTaskFix3 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); Task<Task> outerTask = Task.Factory.StartNew(async () => { await Task.Delay(1000); }); Task innerTask = await outerTask; await innerTask; Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix3 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Dim outerTask As Task(Of Task) = Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function) Dim innerTask As Task = Await outerTask Await innerTask Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End Module
Attente manquante lors d’un appel de retour de tâche
Si vous appelez une méthode de retour de tâche dans une async méthode sans l’attendre, la méthode démarre l’opération asynchrone, mais n’attend pas qu’elle se termine. Le compilateur vous avertit de ce cas avec CS4014 en C# et BC42358 dans Visual Basic :
public static class MissingAwaitExample
{
// BAD: Task.Delay is started but never awaited.
public static async Task PauseOneSecondBuggyAsync()
{
Task.Delay(1000); // CS4014 warning
}
// GOOD: await the task.
public static async Task PauseOneSecondAsync()
{
await Task.Delay(1000);
}
}
Public Module MissingAwaitExample
' BAD: Task.Delay is started but never awaited.
Public Async Function PauseOneSecondBuggyAsync() As Task
Task.Delay(1000) ' Warning BC42358
End Function
' GOOD: Await the task.
Public Async Function PauseOneSecondAsync() As Task
Await Task.Delay(1000)
End Function
End Module
Le stockage du résultat d’une variable supprime l’avertissement, mais ne corrige pas le bogue sous-jacent. Toujours await la tâche, sauf si vous souhaitez intentionnellement un comportement de feu et d’oubli.