退出异步方法是否可以将控制权送回其他异步方法

本文关键字:异步方法 其他 控制权 是否 退出 | 更新日期: 2023-09-27 18:04:21

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

我对我认为会从中得出的结果的解释是正确的,直到某个时候。看起来好像有某种交错发生。我希望 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 应用程序来测试我的代码。

退出异步方法是否可以将控制权送回其他异步方法

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

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

现在,更多细节。当我询问 WinForms 与 WPF 时,我希望您的"预期"输出与实际输出相匹配。我刚刚在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 之间存在差异,而两者都运行消息循环,而我们在这里只处理单线程代码?答案可以在这里找到:

为什么每个调度程序都有唯一的同步上下文。开始调用回调?

WPF 的DispatcherSynchronizationContext.Post只是调用 Dispatcher.BeginInvoke ,WPF 的一个重要实现细节是每个 Dispatcher.BeginInvoke 回调都在其自己唯一的同步上下文中执行,如链接问题中所述。

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

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

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

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

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