退出异步方法可以将控制权返回给不同的异步方法吗?

时间:2014-05-21 13:50:30

标签: c# asynchronous async-await

我理解等待任务的异步会将执行返回给调用者,允许它继续执行直到需要结果。

我对我的想法的解释是正确的,直到某一点。看起来似乎正在进行某种交错。我希望Do3()完成,然后将调用堆栈备份到Do2()。查看结果。

        await this.Go();

哪个电话

        async Task Go()
        {
            await Do1(async () => await Do2("Foo"));

            Debug.WriteLine("Completed async work");
        }

        async Task Do1(Func<Task> doFunc)
        {
            Debug.WriteLine("Start Do1");

            var t = Do2("Bar");

            await doFunc();

            await t;
        }

        async Task Do2(string id)
        {
            Debug.WriteLine("Start Do2: " + id);

            await Task.Yield();

            await Do3(id);

            Debug.WriteLine("End Do2: " + id);
        }

        async Task Do3(string id)
        {
            Debug.WriteLine("Start Do3: " + id);

            await Task.Yield();

            Debug.WriteLine("End Do3: " + id); // I did not expect Do2 to execute here once the method call for Do3() ended
        }

预期结果:

// Start Do1
// Start Do2: Bar
// Start Do2: Foo
// Start Do3: Bar
// Start Do3: Foo
// End Do3: Bar
// End Do2: Bar
// End Do3: Foo
// End Do2: Foo
//Completed async work

实际输出:

//Start Do1
//Start Do2: Bar
//Start Do2: Foo
//Start Do3: Bar
//Start Do3: Foo
//End Do3: Bar
//End Do3: Foo
//End Do2: Bar
//End Do2: Foo
//Completed async work

这里到底发生了什么?

我使用.NET 4.5和一个简单的WPF应用来测试我的代码。

2 个答案:

答案 0 :(得分:2)

这是一个WPF应用程序,所有这些代码都在同一个UI线程上执行。代码中的每个await延续都是通过DispatcherSynchronizationContext.Post来安排的,它会将特殊的Windows消息发布到UI线程的消息队列中。每个延续按照其消息发布的顺序发生(这是特定于实现的,您不应该依赖于此,但这是如何在这里工作的。)

因此,End Do3: Foo的延续确实发布在End Do3: Bar的延续之后。输出是正确的。

现在,更多详细信息。当我询问WinForms vs WPF时,我期待你的预期&#34;输出以匹配实际输出。我刚刚在WinForms下测试它,它确实匹配:

// Start Do1
// Start Do2: Bar
// Start Do2: Foo
// Start Do3: Bar
// Start Do3: Foo
// End Do3: Bar
// End Do2: Bar
// End Do3: Foo
// End Do2: Foo
//Completed async work

那么,为什么WPF和WinForms之间的差异,虽然都运行消息循环,我们只处理单线程代码?答案可以在这里找到:

Why a unique synchronization context for each Dispatcher.BeginInvoke callback?

WPF&#39; DispatcherSynchronizationContext.Post只调用Dispatcher.BeginInvoke,WPF的一个重要实现细节是每个Dispatcher.BeginInvoke回调都在其自己唯一的同步上下文上执行,如在相关问题中解释。

这会影响await对象的Task延续(例如await doFunc())。在WinForms中,这种延续是内联的(同步执行),因为SynchronizationContext.Current保持不变。在WPF中,他们没有内联,而是通过SynchronizationContext.Post发布,因为SynchronizationContext.Current 之前 await task并且在完成task之后不一样(它在task.GetAwaiter().OnCompleted运行时基础结构代码中在await内进行比较)。

因此,在WPF中,它通常是相同的UI线程,但与此线程关联的同步上下文不同,因此延续可能会导致另一个异步PostMessage回调被发布,泵送和执行通过消息循环。除了YieldAwaitable(由Task.Yield返回)之外,您还会遇到从WPF UI线程触发的TaskCompletionSource.SetResult样式延续的此行为。

这是相当复杂但特定于实现的东西。如果您想要精确控制异步await延续的顺序,您可能想要推出自己的同步上下文,类似于Stephen Toub的AsyncPump。虽然通常你不需要,特别是对于UI线程。

答案 1 :(得分:0)

结果并不让我感到惊讶。如果Do3结束,我不明白为什么它应该立即继续Do2。它是异步过程的所有部分,因此实际输出非常有意义。但是,如果不同的运行会产生不同的结果,我不会感到惊讶。

Do3:Bar和Do3:Foo是不同的任务,(可能)在不同的线程中同时执行。完成后,会通知各自的呼叫者,但该通知本身可能也是异步的。 await没有让两个任务在同一个线程中神奇地运行,只是让第一个任务等到另一个完成。因此,当Do2:Bar正在等待该信号时,Do3:Foo可以继续运行并完成。