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 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:
StartNew
. Utilisez Task.Run
place. ContinueWith
. Utilisez await
place. 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.