Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async/await not reacting as expected

Tags:

c#

async-await

Using the code below I expect the string "Finished" to appear before "Ready" on the console. Could anybody explain to me, why await will not wait for finishing the task in this sample?

    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;
    }
like image 882
Alexander Schmidt Avatar asked Oct 25 '11 16:10

Alexander Schmidt


2 Answers

The reason why you're seeing "Finished" after "Ready!" is because of a common confusion point with async methods, and has nothing to do with SynchronizationContexts. SynchronizationContext control which thread things run on, but 'async' has its own very specific rules about ordering. Otherwise programs would go crazy! :)

'await' ensures that the rest of the code in the current async method doesn't execute until after the thing awaited completes. It doesn't promise anything about the caller.

Your async method returns 'void', which is intended for async methods that don't allow for the original caller to rely on method completion. If you want your caller to also wait, you'll need to make sure your async method returns a Task (in case you only want completion/exceptions observed), or a Task<T> if you actually want to return a value as well. If you declare the return type of the method to be either of those two, then the compiler will take care of the rest, about generating a task that represents that method invocation.

For example:

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 print out in order. Here's what's going on:

"A": Before anything else gets called

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

"C": MiddleAsync() is being called, OuterAsync() is still waiting to see if MiddleAsync() is complete or not.

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

"E": DoSomething() is being called, InnerAsync() is still waiting to see if DoSomething() is complete or not. It immediately returns a task, which starts in parallel.

Because of parallelism, there is a race between InnerAsync() finishing its test for completeness on the task returned by DoSomething(), and the DoSomething() task actually starting.

Once DoSomething() starts, it prints out "F", then sleeps for a second.

In the meanwhile, unless thread scheduling is super messed up, InnerAsync() almost certainly has now realized that DoSomething() is not yet complete. Now the async magic starts.

InnerAsync() yanks itself off the callstack, and says that its task is incomplete. This causes MiddleAsync() to yank itself off the callstack and say that its own task is incomplete. This causes OuterAsync() to yank itself off the callstack, and say that its task is incomplete as well.

The task is returned to Main() which notices it's incomplete, and the Wait() call begins.

meanwhile...

On that parallel thread, the old-style TPL Task created in DoSomething() eventually finishes sleeping. It prints out "G".

Once that task gets marked as complete, the rest of InnerAsync() gets scheduled on the TPL to get executed again, and it prints out "H". That completes the task originally returned by InnerAsync().

Once that task gets marked complete, the rest of MiddleAsync() gets scheduled on the TPL to get executed again, and it prints out "I". That completes the task originally returned by MiddleAsync().

Once that task gets marked complete, the rest of OuterAsync() gets scheduled on the TPL to get executed again, and it prints out "J". That completes the task originally returned by OuterAsync().

Since OuterAsync()'s task is now complete, the Wait() call returns, and Main() prints out "K".

Thus even with a little bit of parallelism in the order, C# 5 async still guarantees that the console writing occurs in that exact order.

Let me know if this still seems confusing :)

like image 106
Theo Yaung Avatar answered Sep 29 '22 13:09

Theo Yaung


You're in a console app, so you don't have a specialized SynchronizationContext, which means your continuations will run on Thread Pool threads.

Also, you are not awaiting the call to TestAsync() in Main. This means that when you execute this line:

await DoSomething();

the TestAsync method returns control to Main, which just continues executing normally - i.e. it outputs "Ready!" and waits for a key press.

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

like image 32
Nick Butler Avatar answered Sep 29 '22 11:09

Nick Butler