Async /等待没有按预期反应

时间:2011-10-25 16:16:56

标签: c# async-await

使用下面的代码我希望字符串“Finished”出现在控制台上的“Ready”之前。有人可以向我解释,为什么等待不等待完成这个样本中的任务?

    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;
    }

3 个答案:

答案 0 :(得分:28)

你在“准备好”之后看到“完成”的原因!是因为异步方法常见的混淆点,与SynchronizationContexts无关。 SynchronizationContext控制哪些线程运行,但'async'有自己非常具体的有关排序的规则。程序会变得疯狂! :)

'await'确保当前异步方法中的其余代码在等待完成之后才会执行。它不会对来电者做出任何承诺。

您的异步方法返回'void',它适用于不允许原始调用方依赖方法完成的异步方法。如果您希望调用者也等待,您需要确保异步方法返回Task(如果您只想要观察完成/例外),或者Task<T>如果您确实想要也返回一个值。如果将方法的返回类型声明为这两者中的任何一个,那么编译器将负责其余的工作,关于生成表示该方法调用的任务。

例如:

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");
        });
}

在上面的代码中,“A”到“K”将按顺序打印出来。这是正在发生的事情:

“A”:在调用任何其他内容之前

“B”:正在调用OuterAsync(),Main()仍在等待。

“C”:正在调用MiddleAsync(),OuterAsync()仍在等待看看MiddleAsync()是否完整。

“D”:正在调用InnerAsync(),MiddleAsync()仍在等待看看InnerAsync()是否完整。

“E”:正在调用DoSomething(),InnerAsync()仍在等待DoSomething()是否完成。它会立即返回一个任务,该任务以 parallel 开始。

由于并行性,在InnerAsync()完成对DoSomething()返回的任务的完整性测试以及实际启动的DoSomething()任务之间存在竞争。

一旦DoSomething()启动,它会输出“F”,然后再睡一秒钟。

同时,除非线程调度超级混乱,否则InnerAsync()几乎肯定已经意识到DoSomething() 尚未完成 。现在,异步魔法开始了。

InnerAsync()将自己从callstack中拉出来,并说它的任务不完整。 这会导致MiddleAsync()从callstack中退出并说它自己的任务不完整。 这导致OuterAsync()将自己从callstack中拉出来,并说它的任务也是不完整的。

任务返回Main(),注意到它不完整,Wait()调用开始。

同时...

在该并行线程上,在DoSomething()中创建的旧式TPL任务最终完成休眠。它打印出“G”。

一旦该任务被标记为完成,其余的InnerAsync()将在TPL上进行调度以再次执行,并打印出“H”。这样就完成了InnerAsync()最初返回的任务。

任务标记完成后,其余的MiddleAsync()将在TPL上进行调度以再次执行,并打印出“I”。这完成了MiddleAsync()最初返回的任务。

一旦 任务标记为完成,其余的OuterAsync()就会在TPL上进行调度以再次执行,并打印出“J”。这样就完成了OuterAsync()最初返回的任务。

由于OuterAsync()的任务现在完成,Wait()调用返回,Main()打印出“K”。

因此,即使在顺序中有一点并行性,C#5异步仍然保证控制台写入按照确切的顺序发生。

让我知道这是否仍然令人困惑:)

答案 1 :(得分:16)

您在控制台应用中,因此您没有专门的SynchronizationContext,这意味着您的延续将在线程池线程上运行。

此外,您还没有等待TestAsync()Main的来电。这意味着当您执行此行时:

await DoSomething();

TestAsync方法将控制权返回给Main,它只是继续正常执行 - 即它输出“Ready!”并等待按键。

与此同时,DoSomething完成后的第二秒,await中的TestAsync将继续在线程池线程中输出“已完成”。

答案 2 :(得分:1)

正如其他人所说,控制台程序使用默认的SynchronizationContext,因此await创建的延续被安排到线程池。

您可以使用Nito.AsyncEx library中的AsyncContext来提供简单的异步上下文:

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

另见this related question