ConfigureAwait (false) vs définition du contexte de synchronisation sur null

Il est souvent recommandé pour le code de bibliothèque asynchrone d’utiliser ConfigureAwait(false) pour tous les appels asynchrones afin d’éviter les situations dans lesquelles le retour de notre appel sera planifié sur un thread d’interface utilisateur ou dans un contexte de synchronisation de requêtes Web, provoquant notamment des blocages. .

Un des problèmes liés à l’utilisation de ConfigureAwait(false) est que vous ne pouvez pas le faire simplement au point d’entrée de votre appel à la bibliothèque. Pour qu’il soit efficace, il doit être fait dans l’ensemble de votre code de bibliothèque.

Il me semble qu’une alternative viable consiste à définir simplement le contexte de synchronisation actuel sur null aux points d’entrée de la bibliothèque faisant face au public, au niveau supérieur, sans oublier ConfigureAwait(false) . Cependant, je ne vois pas beaucoup de cas où des personnes adoptent ou recommandent cette approche.

Y-a-t-il quelque chose qui cloche dans la définition du contexte de synchronisation actuel sur null aux points d’entrée de la bibliothèque? Existe-t-il des problèmes potentiels avec cette approche (mis à part l’éventuel impact insignifiant sur les performances de la publication d’un message en attente dans le contexte de synchronisation par défaut)?

(EDIT # 1) Ajout d’un exemple de code de ce que je veux dire:

  public class Program { public static void Main(ssortingng[] args) { SynchronizationContext.SetSynchronizationContext(new LoggingSynchronizationContext(1)); Console.WriteLine("Executing library code that internally clears synchronization context"); //First try with clearing the context INSIDE the lib RunTest(true).Wait(); //Here we again have the context intact Console.WriteLine($"After First Call Context in Main Method is {SynchronizationContext.Current?.ToSsortingng()}"); Console.WriteLine("\nExecuting library code that does NOT internally clear the synchronization context"); RunTest(false).Wait(); //Here we again have the context intact Console.WriteLine($"After Second Call Context in Main Method is {SynchronizationContext.Current?.ToSsortingng()}"); } public async static Task RunTest(bool clearContext) { Console.WriteLine($"Before Lib call our context is {SynchronizationContext.Current?.ToSsortingng()}"); await DoSomeLibraryCode(clearContext); //The rest of this method will get posted to my LoggingSynchronizationContext //But....... if(SynchronizationContext.Current == null){ //Note this will always be null regardless of whether we cleared it or not Console.WriteLine("We don't have a current context set after return from async/await"); } } public static async Task DoSomeLibraryCode(bool shouldClearContext) { if(shouldClearContext){ SynchronizationContext.SetSynchronizationContext(null); } await DelayABit(); //The rest of this method will be invoked on the default (null) synchronization context if we elected to clear the context //Or it should post to the original context otherwise Console.WriteLine("Finishing library call"); } public static Task DelayABit() { return Task.Delay(1000); } } public class LoggingSynchronizationContext : SynchronizationContext { readonly int contextId; public LoggingSynchronizationContext(int contextId) { this.contextId = contextId; } public override void Post(SendOrPostCallback d, object state) { Console.WriteLine($"POST TO Synchronization Context (ID:{contextId})"); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine($"Post Synchronization Context (ID:{contextId})"); base.Send(d, state); } public override ssortingng ToSsortingng() { return $"Context (ID:{contextId})"; } } 

L’exécution de ceci produira:

 Executing library code that internally clears synchronization context Before Lib call our context is Context (ID:1) Finishing library call POST TO Synchronization Context (ID:1) We don't have a current context set after return from async/await After First Call Context in Main Method is Context (ID:1) Executing library code that does NOT internally clear the synchronization context Before Lib call our context is Context (ID:1) POST TO Synchronization Context (ID:1) Finishing library call POST TO Synchronization Context (ID:1) We don't have a current context set after return from async/await After Second Call Context in Main Method is Context (ID:1) 

Tout fonctionne comme prévu, mais je ne rencontre pas de personnes qui recommandent aux bibliothèques de le faire en interne. Je trouve qu’exiger que tous les points d’attente internes soient appelés avec ConfigureAwait(false) est gênant et même l’absence de ConfigureAwait() manquée peut causer des problèmes dans l’ensemble de l’application. Cela semble résoudre le problème simplement au niveau du point d’entrée public de la bibliothèque avec une seule ligne de code. Qu’est-ce que je rate?

(EDIT # 2)

D’après certains commentaires de la réponse d’Alexei, il semble que je n’ai pas envisagé la possibilité qu’une tâche ne soit pas immédiatement attendue. Étant donné que le contexte d’exécution est capturé au moment de l’attente (et non à l’heure de l’appel async), cela signifierait que la modification apscope à SynchronizationContext.Current ne serait pas isolée à la méthode de bibliothèque. Sur cette base, il semblerait qu’il devrait suffire de forcer une capture du contexte en englobant la logique interne de la bibliothèque dans un appel qui impose une attente. Par exemple:

  async void button1_Click(object sender, EventArgs e) { var getSsortingngTask = GetSsortingngFromMyLibAsync(); this.textBox1.Text = await getSsortingngTask; } async Task GetSsortingngFromMyLibInternal() { SynchronizationContext.SetSynchronizationContext(null); await Task.Delay(1000); return "HELLO WORLD"; } async Task GetSsortingngFromMyLibAsync() { //This forces a capture of the current execution context (before synchronization context is nulled //This means the caller's context should be intact upon return //even if not immediately awaited. return await GetSsortingngFromMyLibInternal(); } 

(EDIT # 3)

Sur la base de la discussion sur la réponse de Stephen Cleary. Il y a quelques problèmes avec cette approche. Mais nous pouvons faire une approche similaire en encapsulant l’appel de la bibliothèque dans une méthode non asynchrone qui renvoie toujours une tâche, mais s’occupe de réinitialiser le contexte de synchronisation à la fin. (Notez que ceci utilise le SynchronizationContextSwitcher de la bibliothèque AsyncEx de Stephen.

  async void button1_Click(object sender, EventArgs e) { var getSsortingngTask = GetSsortingngFromMyLibAsync(); this.textBox1.Text = await getSsortingngTask; } async Task GetSsortingngFromMyLibInternal() { SynchronizationContext.SetSynchronizationContext(null); await Task.Delay(1000); return "HELLO WORLD"; } Task GetSsortingngFromMyLibAsync() { using (SynchronizationContextSwitcher.NoContext()) { return GetSsortingngFromMyLibInternal(); } //Context will be restored by the time this method returns its task. } 

Il est souvent recommandé pour le code de bibliothèque asynchrone d’utiliser ConfigureAwait (false) pour tous les appels asynchrones afin d’éviter les situations dans lesquelles le retour de notre appel sera planifié sur un thread d’interface utilisateur ou dans un contexte de synchronisation de requêtes Web, provoquant notamment des blocages. .

Je recommande ConfigureAwait(false) car il (correctement) note que le contexte d’appel n’est pas requirejs. Cela vous donne également un petit avantage en termes de performances. Bien que ConfigureAwait(false) puisse empêcher les blocages, ce n’est pas le but recherché.

Il me semble qu’une alternative viable consiste à définir simplement le contexte de synchronisation actuel sur null aux points d’entrée de la bibliothèque faisant face au public, au niveau supérieur, sans oublier ConfigureAwait (false).

Oui, c’est une option. Cela TaskScheduler.Current pas complètement les blocages, car TaskScheduler.Current tentera de reprendre sur TaskScheduler.Current s’il n’y a pas de SynchronizationContext actuel .

En outre, il est inacceptable qu’une bibliothèque remplace un composant de niveau cadre.

Mais vous pouvez le faire si vous voulez. N’oubliez pas de rétablir sa valeur d’origine à la fin.

Oh, un autre piège: il y a des API qui vont supposer que le SyncCtx actuel est ce qui est fourni pour ce framework. Certaines API d’assistance ASP.NET sont comme ça. Donc, si vous rappelez le code de l’utilisateur final, cela pourrait poser problème. Mais dans ce cas, vous devez explicitement documenter le contexte dans lequel leurs rappels sont invoqués.

Cependant, je ne vois pas beaucoup de cas où des personnes adoptent ou recommandent cette approche.

Il devient lentement plus populaire. Assez pour que j’ai ajouté une API pour cela dans ma bibliothèque AsyncEx :

 using (SynchronizationContextSwitcher.NoContext()) { ... } 

Je n’ai toutefois pas utilisé cette technique moi-même.

Existe-t-il des problèmes potentiels avec cette approche (mis à part l’éventuel impact insignifiant sur les performances de la publication d’un message en attente dans le contexte de synchronisation par défaut)?

En réalité, c’est un gain de performance insignifiant.

Le contexte de synchronisation est similaire à la variable statique et sa modification sans restauration avant que le contrôle ne quitte votre méthode entraînera un comportement inattendu.

Je ne crois pas que vous puissiez définir en toute sécurité le contexte de synchronisation du thread en cours dans une fonction de bibliothèque qui await rien, car la restauration du contexte au milieu du code généré par le compilateur n’est pas vraiment possible.

Échantillon:

  async Task MyLibraryMethodAsync() { SynchronizationContext.SetSynchronizationContext(....); await SomeInnerMethod(); // note that method returns at this point // maybe restore synchronization context here... return 42; } ... // code that uses library, runs on UI thread void async OnButtonClick(...) { // <-- context here is UI-context, code running on UI thread Task doSomething = MyLibraryMethodAsync(); // <-- context here is set by MyLibraryMethod - ie null, code running on UI thread var textFromFastService = await FastAsync(); // <-- context here is set by MyLibraryMethod, code running on pool thread (non-UI) textBox7.Text = textFromFastService; // fails... var get42 = await doSomething; }