Async / wait does not respond as expected

Using the following code, I expect the Finish line to appear before the Finish line in the console. Can someone explain to me why to wait without waiting for the task to complete in this example?

static void Main(string[] 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; } 
+6
source share
3 answers

The reason you see Done after Done! due to a common point of confusion with asynchronous methods and has nothing to do with SynchronizationContexts. SynchronizationContext controls which threads work, but async has its own very specific ordering rules. Otherwise, the programs are crazy! :)

'await' ensures that the rest of the code in the current asynchronous method will not be executed until the completed thing completes. This does not promise anything about the caller.

Your async method returns 'void', which is intended for asynchronous methods that do not allow the original caller to rely on the completion of the method. If you want your caller to also wait, you need to make sure your async method returns Task (in case you want to get only completion / exceptions) or Task<T> if you really want to return a value like Well. If you declare the return type of the method as one of two, then the compiler will take care of the rest, creating a task that represents a call to this method.

For instance:

 static void Main(string[] 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"); }); } 

In the above code, โ€œAโ€ through โ€œKโ€ will be printed in order. Here's what happens:

"A": Before anything else is called

"B": OuterAsync () is called, the Main () function is still waiting.

"C": MiddleAsync () function is called, OuterAsync () is still waiting for MiddleAsync () to complete or not.

"D": InnerAsync () is called, MiddleAsync () is still waiting for InnerAsync () to complete or not.

"E": DoSomething () is called, InnerAsync () is still waiting for DoSomething () to complete or not. It immediately returns a task that starts with parallel .

Due to the parallelism between InnerAsync (), the test for completeness of the task returned by DoSomething () ends and the DoSomething () task actually runs.

As soon as DoSomething () starts, it prints "F", then sleeps for a second.

In the meantime, if thread scheduling is not confused, InnerAsync () will almost certainly now realize that DoSomething () is not yet complete . Asynchronous magic now begins.

InnerAsync () is removed from the stop call and says that its task is incomplete. This causes MiddleAsync () to push itself out of the callstack and says that its own task is incomplete. This causes OuterAsync () to push itself out of the freeze frame and says that its task is also incomplete.

The task returns Main (), which notices its incomplete, and the Wait () call begins.

meanwhile ...

In this parallel thread, the old TPL task created in DoSomething () eventually ends up sleeping. He is typing "G".

Once this task is marked as complete, the rest of InnerAsync () will be assigned to the TPL for re-execution, and it will print "H". This completes the task originally returned by InnerAsync ().

After this task is marked complete, the rest of MiddleAsync () will be assigned to the TPL to restart, and it will print "I". This completes the task originally returned by MiddleAsync ().

After this task is complete, the rest of OuterAsync () will be assigned to the TPL for re-execution, and it will print "J". This completes the task originally returned by OuterAsync ().

Since the OuterAsync () task has completed, the Wait () call returns, and Main () prints "K".

That way, even with a little parallelism in order, asynchronous C # 5 still ensures that the console writes in this exact order.

Let me know if this still seems confusing :)

+27
source

You are in a console application, so you do not have a specialized SynchronizationContext , which means that your continuations will be executed in the threads of the thread pool.

In addition, you do not expect TestAsync() to be called in Main . This means that when executing this line:

 await DoSomething(); 

The TestAsync method returns control to Main , which only continues to execute normally, that is, it displays "Ready!". and waits for a keystroke.

Meanwhile, a second later, when DoSomething completes, await in TestAsync will continue in the thread pool thread and display Finished.

+16
source

As others have noted, console programs use the default value SynchronizationContext , so the extensions created by await are obtained in the thread pool.

You can use AsyncContext from my Nito.AsyncEx library to provide a simple asynchronous context:

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

Also see this related question .

+1
source

Source: https://habr.com/ru/post/900061/


All Articles