退出异步方法是否可以将控制权送回其他异步方法
本文关键字:异步方法 其他 控制权 是否 退出 | 更新日期: 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可以继续运行并完成。