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.
Lorsqu’une bibliothèque expose uniquement des API asynchrones, les consommateurs les encapsulent parfois dans des appels synchrones pour satisfaire une interface ou un contrat synchrone. Ce modèle « sync-over-async » peut sembler simple, mais il s’agit d’une source courante d’interblocages et de problèmes de performances.
Modèles de base d'enveloppement
Un wrapper synchrone autour d’une méthode TAP (Task-based Asynchrone Pattern) accède à la propriété de Result la tâche, ce qui bloque le thread appelant :
public class TapWrapper
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
}
Public Module TapWrapper
Public Function Foo(fooAsync As Func(Of Task(Of Integer))) As Integer
Return fooAsync().Result
End Function
End Module
Cette approche semble simple, mais elle peut entraîner de graves problèmes en fonction de l’environnement dans lequel il s’exécute.
Interblocages avec des contextes à thread unique
Le scénario le plus dangereux se produit lorsque vous appelez un wrapper synchrone à partir d’un thread qui a un thread SynchronizationContextunique . Ce scénario est généralement un thread d’interface utilisateur dans des applications WPF, Windows Forms ou MAUI.
public static class DeadlockExample
{
private static void Delay(int milliseconds)
{
DelayAsync(milliseconds).Wait();
}
private static async Task DelayAsync(int milliseconds)
{
await Task.Delay(milliseconds);
}
}
Public Module DeadlockExample
Private Sub Delay(milliseconds As Integer)
DelayAsync(milliseconds).Wait()
End Sub
Private Async Function DelayAsync(milliseconds As Integer) As Task
Await Task.Delay(milliseconds)
End Function
End Module
Voici ce qui se passe pas à pas :
- Le thread d’interface utilisateur appelle
Delay, qui appelleDelayAsync(milliseconds).Wait(). -
DelayAsyncs’exécute de manière synchrone jusqu’à ce qu’elle atteigneawait Task.Delay(milliseconds). - Étant donné que le délai n’est pas encore terminé,
awaitcapture l'état actuel SynchronizationContext puis se suspend.DelayAsyncretourne un Task à l’appelant. - Le thread UI se bloque en
.Wait()en attendant que cette tâche se termine. - Une fois le délai terminé, la continuation doit s’exécuter sur l’original
SynchronizationContextqui est le thread d’interface utilisateur. - Le thread d’interface utilisateur ne peut pas traiter la continuation, car elle est bloquée dans
.Wait(). - Impasse.
Important
La réussite ou l’échec du code sync-over-async dépend de l’environnement dans lequel il s’exécute. Le code qui fonctionne dans une application console peut entraîner un blocage sur un thread UI ou dans ASP.NET (du .NET Framework). Cette dépendance environnementale est une raison essentielle pour éviter d’exposer des wrappers synchrones.
Épuisement du pool de threads
Les interblocages ne sont pas limités aux threads d’interface utilisateur. Si une méthode asynchrone dépend du pool de threads pour accomplir son travail, par exemple, en mettant en file d’attente une étape de traitement finale, le fait de bloquer de nombreux threads du pool avec des wrappers synchrones peut entraîner une saturation du pool.
public static class ThreadPoolDeadlockExample
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
public static async Task DemonstrateDeadlockRiskAsync()
{
var tasks = Enumerable.Range(0, 25)
.Select(_ => Task.Run(() => Foo(() => SomeIOOperationAsync())));
await Task.WhenAll(tasks);
}
private static async Task<int> SomeIOOperationAsync()
{
await Task.Delay(100);
return 42;
}
}
Dans ce scénario :
- De nombreux threads de pool de threads appellent
Foo, qui sont bloqués dans.Result. - Chaque opération asynchrone termine ses opérations d’entrées/sorties et a besoin d’un thread du pool pour exécuter son rappel d’achèvement.
- Étant donné que les appels bloqués occupent les threads de travail disponibles, les achèvements peuvent attendre longtemps qu’un thread devienne disponible.
- Le .NET moderne peut ajouter davantage de threads au pool de threads au fil du temps, mais l’application peut toujours souffrir d'une grave pénurie de threads, d'un faible débit, de longs retards ou d'un gel apparent.
Ce modèle a affecté HttpWebRequest.GetResponse dans .NET Framework 1.x, où la méthode synchrone a été implémentée en tant que wrapper autour du BeginGetResponse/EndGetResponse asynchrone.
Directive : éviter d’exposer des wrappers synchrones
N’exposez pas de méthode synchrone qui encapsule une implémentation asynchrone. Au lieu de cela, laissez au consommateur la décision de bloquer ou non. Le consommateur connaît son environnement d'exécution des threads et peut faire un choix éclairé.
Si vous avez besoin d’appeler une méthode asynchrone de manière synchrone, pensez d’abord à restructurer le code pour qu’il soit « asynchrone tout au bas ». La refactorisation est souvent la meilleure solution à long terme.
Stratégies d’atténuation lorsque la synchronisation sur async est inévitable
Parfois, la synchronisation sur async est vraiment inévitable. Par exemple, il est inévitable lorsque vous implémentez une interface qui nécessite une méthode synchrone et que la seule implémentation disponible est asynchrone. Dans ces cas, appliquez les stratégies suivantes pour réduire le risque.
Utiliser ConfigureAwait(false) dans l’implémentation asynchrone
Si vous contrôlez la méthode asynchrone, utilisez Task.ConfigureAwait avec false sur chaque await pour empêcher la continuation de rebasculement vers l'original SynchronizationContext.
public static class ConfigureAwaitMitigation
{
public static async Task<int> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return 42;
}
public static int Sync()
{
return LibraryMethodAsync().GetAwaiter().GetResult();
}
}
Public Module ConfigureAwaitMitigation
Public Async Function LibraryMethodAsync() As Task(Of Integer)
Await Task.Delay(100).ConfigureAwait(False)
Return 42
End Function
Public Function Sync() As Integer
Return LibraryMethodAsync().Result
End Function
End Module
En tant qu’auteur de bibliothèque, utilisez ConfigureAwait(false) sur tous les await, sauf si votre code doit spécifiquement récupérer dans le contexte sauvegardé. L’utilisation ConfigureAwait(false) est une bonne pratique pour les performances et permet d’empêcher les blocages lorsque les consommateurs bloquent.
Décharger vers le pool de threads
Si vous ne contrôlez pas l'implémentation asynchrone (et qu'elle pourrait ne pas utiliser ConfigureAwait(false)), confiez l'appel au pool de threads. Le pool de threads n’a pas de SynchronizationContext ; par conséquent, l’attente n’essaie pas de rediriger vers un thread bloqué.
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Tester dans plusieurs environnements
Si vous devez expédier un wrapper synchrone, testez-le à partir de :
- Thread d’interface utilisateur (WPF, Windows Forms).
- Pool de threads en cours de chargement.
- Pool de threads avec un nombre de threads maximal faible.
- Une application console.
Le comportement qui fonctionne dans un environnement peut se bloquer dans un autre.