Async / wait ne réagit pas comme prévu

En utilisant le code ci-dessous, la chaîne “Terminé” devrait précéder “Prêt” sur la console. Quelqu’un pourrait-il m’expliquer, pourquoi attendre n’attendra-t-il pas pour terminer la tâche dans cet exemple?

static void Main(ssortingng[] args) { TestAsync(); Console.WriteLine("Ready!"); Console.ReadKey(); } private async static void TestAsync() { await DoSomething(); Console.WriteLine("Finished"); } private static Task DoSomething() { var ret = Task.Run(() => { for (int i = 1; i < 10; i++) { Thread.Sleep(100); } }); return ret; } 

La raison pour laquelle vous voyez “Terminé” après “Prêt!” Cela est dû à un sharepoint confusion commun avec les méthodes async et n’a rien à voir avec SynchronizationContexts. SynchronizationContext contrôle le thread sur lequel les choses s’exécutent, mais ‘async’ a ses propres règles très spécifiques concernant la commande. Sinon, les programmes deviendraient fous! 🙂

‘wait’ garantit que le rest du code de la méthode async actuelle ne sera pas exécuté avant la fin de la tâche attendue. Cela ne promet rien à propos de l’appelant.

Votre méthode async retourne ‘void’, ce qui est destiné aux méthodes asynchrones qui ne permettent pas à l’appelant d’origine de se fier à l’achèvement de la méthode. Si vous souhaitez que votre appelant attende également, vous devez vous assurer que votre méthode asynchrone renvoie une Task (si vous souhaitez uniquement observer l’achèvement / les exceptions) ou une Task si vous souhaitez également renvoyer une valeur. . Si vous déclarez que le type de retour de la méthode est l’un ou l’autre, le compilateur se chargera du rest, à propos de la génération d’une tâche qui représente cet appel de méthode.

Par exemple:

 static void Main(ssortingng[] args) { Console.WriteLine("A"); // in .NET, Main() must be 'void', and the program terminates after // Main() returns. Thus we have to do an old fashioned Wait() here. OuterAsync().Wait(); Console.WriteLine("K"); Console.ReadKey(); } static async Task OuterAsync() { Console.WriteLine("B"); await MiddleAsync(); Console.WriteLine("J"); } static async Task MiddleAsync() { Console.WriteLine("C"); await InnerAsync(); Console.WriteLine("I"); } static async Task InnerAsync() { Console.WriteLine("D"); await DoSomething(); Console.WriteLine("H"); } private static Task DoSomething() { Console.WriteLine("E"); return Task.Run(() => { Console.WriteLine("F"); for (int i = 1; i < 10; i++) { Thread.Sleep(100); } Console.WriteLine("G"); }); } 

Dans le code ci-dessus, "A" à "K" seront imprimés dans l'ordre. Voici ce qui se passe:

"A": Avant que rien ne soit appelé

"B": OuterAsync () est appelé, Main () est toujours en attente.

"C": MiddleAsync () est appelée, OuterAsync () attend toujours de voir si MiddleAsync () est complet ou non.

"D": InnerAsync () est appelée, MiddleAsync () attend toujours de voir si InnerAsync () est complet ou non.

"E": DoSomething () est appelé, InnerAsync () attend toujours de voir si DoSomething () est complet ou non. Il renvoie immédiatement une tâche qui commence en parallèle .

En raison du parallélisme, il y a une course entre InnerAsync () qui termine son test de complétude sur la tâche renvoyée par DoSomething () et la tâche DoSomething () qui commence réellement.

Une fois que DoSomething () commence, il affiche "F", puis dort pendant une seconde.

En attendant, sauf si la planification des threads est trop compliquée, InnerAsync () a presque certainement maintenant compris que DoSomething () n'est pas encore complet . Maintenant, la magie asynchrone commence.

InnerAsync () se retire de la stack d'appels et dit que sa tâche est incomplète. Cela amène MiddleAsync () à se retirer de la stack d'appels et à dire que sa propre tâche est incomplète. Cela force OuterAsync () à se dégager de la stack d'appels et à dire que sa tâche est également incomplète.

La tâche est renvoyée à Main () qui constate qu'elle est incomplète et l'appel Wait () commence.

pendant ce temps...

Sur ce thread parallèle, l'ancienne tâche TPL créée dans DoSomething () finit par s'endormir. Il affiche "G".

Une fois que cette tâche est marquée comme terminée, le rest de InnerAsync () est planifié sur la TPL pour être exécuté à nouveau et il affiche "H". Cela termine la tâche initialement renvoyée par InnerAsync ().

Une fois que cette tâche est marquée comme terminée, le rest de MiddleAsync () est planifié sur la TPL pour être exécuté à nouveau et il affiche "I". Cela termine la tâche initialement renvoyée par MiddleAsync ().

Une fois que cette tâche est marquée comme terminée, le rest de OuterAsync () est planifié sur la TPL pour être exécuté à nouveau et il affiche "J". Cela termine la tâche initialement renvoyée par OuterAsync ().

La tâche de OuterAsync () étant maintenant terminée, l'appel Wait () est renvoyé et Main () affiche "K".

Ainsi, même avec un peu de parallélisme dans l'ordre, C # 5 async garantit toujours que l'écriture de la console se produit dans cet ordre exact.

Faites-moi savoir si cela semble encore déroutant 🙂

Vous êtes dans une application console, vous n’avez donc pas de SynchronizationContext spécialisé, ce qui signifie que vos continuations seront exécutées sur les threads du pool de threads.

En outre, vous TestAsync() pas l’appel à TestAsync() dans Main . Cela signifie que lorsque vous exécutez cette ligne:

 await DoSomething(); 

la méthode TestAsync renvoie le contrôle à Main , qui continue simplement à s’exécuter normalement, c’est-à-dire qu’il TestAsync le TestAsync “Prêt!” et attend un appui sur une touche.

Pendant ce temps, une seconde plus tard, lorsque DoSomething terminé, l’ await dans TestAsync continue sur un thread de pool de threads et génère “Terminé”.

Comme d’autres l’ont noté, les programmes de console utilisent le SynchronizationContext par défaut. SynchronizationContext suites créées par await sont donc planifiées dans le pool de threads.

Vous pouvez utiliser AsyncContext partir de ma bibliothèque Nito.AsyncEx pour fournir un contexte async simple:

 static void Main(ssortingng[] args) { Nito.AsyncEx.AsyncContext.Run(TestAsync); Console.WriteLine("Ready!"); Console.ReadKey(); } 

Voir aussi cette question connexe .