Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
177 views
in Technique[技术] by (71.8m points)

c# - Async/await not reacting as expected

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;
    }
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 :)


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...