Letterman
Letterman

Reputation: 4206

Unexpected behaviour after returning from await

I know there are a lot of questions about async/await, but I couldn't find any answer to this.

I've encountered something I don't understand, consider the following code:

void Main()
{
    Poetry();
    while (true)
    {
        Console.WriteLine("Outside, within Main.");
        Thread.Sleep(200);
    }
}

async void Poetry()
{
   //.. stuff happens before await
   await Task.Delay(10);
   for (int i = 0; i < 10; i++)
   {
       Console.WriteLine("Inside, after await.");
       Thread.Sleep(200);
   }
}

Obviously, on the await operator, the control returns to the caller, while the method being awaited, is running on the background. (assume an IO operation)

But after the control comes back to the await operator, the execution becomes parallel, instead of (my expectation) remaining single-threaded.

I'd expect that after "Delay" has been finished the thread will be forced back into the Poetry method, continues from where it left.

Which it does. The weird thing for me, is why the "Main" method keeps running? is that one thread jumping from one to the other? or are there two parallel threads?

Isn't it a thread-safety problem, once again?

I find this confusing. I'm not an expert. Thanks.

Upvotes: 5

Views: 1270

Answers (4)

Letterman
Letterman

Reputation: 4206

I'd like to answer my own question here.

Some of you gave me great answers which all helped me understand better (and were thumbed up). Possibly no one gave me a full answer because I've failed to ask the full question. In any case someone will encounter my exact misunderstanding, I'd like this to be the first answer (but I'll recommend to look at some more answers below).

So, Task.Delay uses a Timer which uses the operating system to fire an event after N milliseconds. after this period a new pooled thread is created, which basically does almost nothing.

The await keyword means that after the thread has finished (and it's doing almost nothing) it should continue to whatever comes after the await keyword.

Here comes the synchronization context, as mentioned in other answers.

If there is no such context, the same newly-created-pooled-thread will continue running what ever comes after the await.

If there is a synchronizing context, the newly-created-pool-thread, will only push whatever comes after the await, into synchronizing context.

For the sake of it, here are a few points I didn't realize:

  • The async/await are not doing anything which wasn't (technologly speaking) possible before. Just maybe amazingly clumsy.
  • It's is just a language support for some of .NET 4.5 classes.
  • It's much like yield return. It may break your method into a few methods, and may even generate a class behind, and use some methods from the BCL, but nothing more.

Anyway, I recommend reading C# 5.0 In A Nutshell's chapter "Concurrency and Asynchrony". It helped me a lot. It is great, and actually explains the whole story.

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 456477

I have a description on my blog of how async methods resume after an await. In essence, await captures the current SynchronizationContext unless it is null in which case it captures the current TaskScheduler. That "context" is then used to schedule the remainder of the method.

Since you're executing a Console app, there is no SynchronizationContext, and the default TaskScheduler is captured to execute the remainder of the async method. That context queues the async method to the thread pool. It is not possible to return to the main thread of a Console app unless you actually give it a main loop with a SynchronizationContext (or TaskScheduler) that queues to that main loop.

Upvotes: 8

noseratio
noseratio

Reputation: 61666

Read It's All About the SynchronizationContext and I'm sure it'll become less confusing. The behavior you're seeing makes perfect sense. Task.Delay uses Win32 Kernel Timer APIs internally (namely, CreateTimerQueueTimer). The timer callback is invoked on a pool thread, different from your Main thread. That's where the rest of Poetry after await continues executing. This is how the default task scheduler works, in the absence of synchronization context on the original thread which initiated the await.

Because you don't do await the Poetry() task (and you can't unless you return Task instead of void), its for loop continues executing in parallel with the while loop in your Main. Why, and more importantly, how would you expect it to be "forced" back onto the Main thread? There has to be some explicit point of synchronization for this to happen, the thread cannot simply get interrupted in the middle of the while loop.

In a UI application, the core message loop may serve as such kind of synchronization point. E.g. for a WinForms app, WindowsFormsSynchronizationContext would make this happen. If await Task.Delay() is called on the main UI thread, the code after await would asynchronously continue on the main UI thread, upon some future iteration of the message loop run by Application.Run.

So, if it was a UI thread, the rest of the Poetry wouldn't get executed in parallel with the while loop following the Poetry() call. Rather, it would be executed when the control flow had returned to the message loop. Or, you might explicitly pump messages with Application.DoEvents() for the continuation to happen, although I wouldn't recommend doing that.

On a side note, don't use async void, rather use async Task, more info.

Upvotes: 3

drew_w
drew_w

Reputation: 10430

When you call an async routine the purpose of this is to allow the program to run a method while still allowing the calling routine, form or application to continue to respond to user input (in other words, continue execution normally). The "await" keyword pauses execution at the point it is used, runs the task using another thread then returns to that line when the thread completes.

So, in your case if you want the main routine to pause until the "Poetry" routine is done you need to use the await keyword something like this:

void async Main()
{
   await Poetry();
   while (true)
   {
       Console.WriteLine("Outside, within Main.");
      Thread.Sleep(200);
   }
}

You will also need to change the definition for Poetry to allow the await keyword to be used:

async Task Poetry()

Because this question really intrigued me I went ahead and wrote an example program you can actually compile. Just create a new console application and paste this example in. You can see the result of using "await" versus not using it.

class Program
{
    static void Main(string[] args)
    {
        RunMain();

        // pause long enough for all async routines to complete (10 minutes)
        System.Threading.Thread.Sleep(10 * 60 * 1000);
    }

    private static async void RunMain()
    {
        // with await this will pause for poetry
        await Poetry();

        // without await this just runs
        // Poetry();

        for (int main = 0; main < 25; main++)
        {
            System.Threading.Thread.Sleep(10);
            Console.WriteLine("MAIN [" + main + "]");
        }
    }

    private static async Task Poetry()
    {
        await Task.Delay(10);
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("IN THE POETRY ROUTINE [" + i + "]");
            System.Threading.Thread.Sleep(10);
        }
    }
}

Happy testing! Oh, and you can still read more information here.

Upvotes: 1

Related Questions