ConfigureAwait (False) ne change pas le contexte après ContinueWith ()

Je ne sais pas si je fais quelque chose de mal ou si j’ai trouvé un bogue dans la bibliothèque Async, mais j’ai constaté un problème lors de l’exécution de code async après être revenu au contexte Synchronisé avec continueWith ().

UPDATE: Le code s’exécute maintenant

using System; using System.ComponentModel; using System.Net.Http; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication1 { internal static class Program { [STAThread] private static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } public partial class Form1 : Form { public Form1() { InitializeComponent(); MainFrameController controller = new MainFrameController(this); //First async call without continueWith controller.DoWork(); //Second async call with continueWith controller.DoAsyncWork(); } public void Callback(Task task) { Console.Write(task.Result); //IT WORKS MainFrameController controller = new MainFrameController(this); //third async call controller.DoWork(); //IT WILL DEADLOCK, since ConfigureAwait(false) in HttpClient DOESN'T change context } } internal class MainFrameController { private readonly Form1 form; public MainFrameController(Form1 form) { this.form = form; } public void DoAsyncWork() { Task task = Task.Factory.StartNew(() => DoWork()); CallbackWithAsyncResult(task); } private void CallbackWithAsyncResult(Task asyncPrerequirejsiteCheck) { asyncPrerequirejsiteCheck.ContinueWith(task => form.Callback(task), TaskScheduler.FromCurrentSynchronizationContext()); } public HttpResponseMessage DoWork() { MyHttpClient myClient = new MyHttpClient(); return myClient.RunAsyncGet().Result; } } internal class MyHttpClient { public async Task RunAsyncGet() { HttpClient client = new HttpClient(); return await client.GetAsync("https://www.google.no").ConfigureAwait(false); } } partial class Form1 { private IContainer components; protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows Form Designer generated code private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Text = "Form1"; } #endregion } } 
  • Le code HttpClient qui est asynchrone s’exécute bien la première fois.
  • Ensuite, je lance le deuxième code async et retourne dans le contexte de l’interface utilisateur avec ContinueWith, et cela fonctionne bien.
  • Je lance à nouveau le code HttClient, mais il se bloque car cette fois, ConfigureAwait (false) ne modifie pas le contexte.

Le principal problème de votre code est dû à StartNew et ContinueWith . ContinueWith est dangereux pour les mêmes raisons que StartNew , comme je le décris sur mon blog.

En résumé: StartNew et ContinueWith ne doivent être utilisés que si vous effectuez un parallélisme dynamic basé sur des tâches (ce que ce code n’est pas).

Le problème réel est que HttpClient.GetAsync n’utilise pas (l’équivalent de) ConfigureAwait(false) ; il utilise ContinueWith avec son argument de planificateur par défaut ( TaskScheduler.Current et non TaskScheduler.Default ).

Pour expliquer plus en détail …

Le planificateur par défaut pour StartNew et ContinueWith n’est pas TaskScheduler.Default (le pool de threads); c’est TaskScheduler.Current (le planificateur de tâches actuel). Ainsi, dans votre code, DoAsyncWork tel qu’il est actuellement n’exécute pas toujours DoWork sur le pool de threads.

La première fois que DoAsyncWork est appelé, il sera appelé sur le thread d’interface utilisateur, mais sans TaskScheduler actuel . Dans ce cas, TaskScheduler.Current est identique à TaskScheduler.Default et DoWork est appelé sur le pool de threads.

CallbackWithAsyncResult appelle ensuite Form1.Callback avec un TaskScheduler qui l’exécute sur le thread d’interface utilisateur. Ainsi, lorsque Form1.Callback appelle DoAsyncWork , il est appelé sur le thread d’interface utilisateur avec un TaskScheduler actuel (le planificateur de tâches d’interface utilisateur). Dans ce cas, TaskScheduler.Current est le planificateur de tâches d’interface utilisateur et DoAsyncWork finit par appeler DoWork sur le thread d’interface utilisateur .

Pour cette raison, vous devez toujours spécifier un TaskScheduler lorsque vous appelez StartNew ou ContinueWith .

Donc, c’est un problème. Mais cela ne provoque pas réellement le blocage que vous voyez, car ConfigureAwait(false) devrait permettre à ce code de simplement bloquer l’UI au lieu de l’interblocage.

C’est une impasse parce que Microsoft a commis la même erreur . Consultez la ligne 198 ici : GetContentAsync (appelé par GetAsync ) utilise ContinueWith sans spécifier de planificateur. Donc, il récupère le TaskScheduler.Current de votre code et ne terminera jamais sa tâche tant qu’il ne pourra pas s’exécuter sur ce planificateur (c’est-à-dire le thread d’interface utilisateur), ce qui entraînera le blocage classique.

Vous ne pouvez rien faire pour corriger le bogue HttpClient.GetAsync (évidemment). Vous devrez juste y TaskScheduler.Current , et le moyen le plus simple de le faire est d’éviter d’avoir un TaskScheduler.Current . Jamais, si vous le pouvez.

Voici quelques instructions générales pour le code asynchrone:

  • N’utilisez jamais StartNew . Utilisez Task.Run place.
  • Ne jamais utiliser ContinueWith . Utilisez await place.
  • N’utilisez jamais Result . Utilisez await place.

Si nous ne faisons que des modifications minimes (remplaçant StartNew par Run et ContinueWith par DoAsyncWork ), alors DoAsyncWork exécute toujours DoWork sur le pool de threads et le blocage est évité (puisque await utilise le SynchronizationContext directement et non un TaskScheduler ):

 public void DoAsyncWork() { Task task = Task.Run(() => DoWork()); CallbackWithAsyncResult(task); } private async void CallbackWithAsyncResult(Task asyncPrerequirejsiteCheck) { try { await asyncPrerequirejsiteCheck; } finally { form.Callback(asyncPrerequirejsiteCheck); } } 

Cependant, il est toujours discutable d’avoir un scénario de rappel avec une asynchronie basée sur une tâche, car les tâches elles-mêmes ont la puissance des rappels qu’elles contiennent. On dirait que vous essayez de faire une sorte d’initialisation asynchrone; J’ai un article de blog sur la construction asynchrone qui montre quelques approches possibles.

Même quelque chose de vraiment basique comme celui-ci serait une meilleure conception que les callbacks (encore une fois, IMO), même s’il utilise async void pour l’initialisation:

 public partial class Form1 : Form { public Form1() { InitializeComponent(); MainFrameController controller = new MainFrameController(); controller.DoWork(); Callback(controller.DoAsyncWork()); } private async void Callback(Task task) { await task; Console.Write(task.Result); MainFrameController controller = new MainFrameController(); controller.DoWork(); } } internal class MainFrameController { public Task DoAsyncWork() { return Task.Run(() => DoWork()); } public HttpResponseMessage DoWork() { MyHttpClient myClient = new MyHttpClient(); var task = myClient.RunAsyncGet(); return task.Result; } } 

Bien entendu, il existe d’autres problèmes de conception ici, à savoir que DoWork bloque une opération naturellement asynchrone et DoAsyncWork bloque un thread de pool de threads lors d’une opération naturellement asynchrone. Ainsi, lorsque Form1 appelle DoAsyncWork , il attend une tâche de pool de threads bloquée lors d’une opération asynchrone. Asynchrone sur synchronisation asynchrone, c’est-à-dire. Vous pouvez également bénéficier de ma série de blogs sur l’étiquette de Task.Run .

Ne pas utiliser .Result . Si vous avez du code qui utilise async / wait, oubliez-le complètement, il existe même. Même si cela fonctionne aujourd’hui, ce que vous essayez de faire sera tellement fragile que cela ne fonctionnera pas nécessairement demain.