这是一个很好的发现——我同意这里的 CTP 中实际上存在一个错误。我深入研究了它,这就是发生的事情:
这是异步编译器转换的 CTP 实现以及 .NET 4.0+ 中 TPL(任务并行库)的现有行为的组合。以下是影响因素:
- 源代码中的 finally 主体被翻译成真正的 CLR-finally 主体的一部分。出于许多原因,这是可取的,其中之一是我们可以让 CLR 执行它,而无需额外时间捕获/重新抛出异常。这也在一定程度上简化了我们的代码生成 - 更简单的代码生成会在编译后生成更小的二进制文件,这绝对是我们的许多客户所希望的。:)
Task
该方法的首要Func(int n)
任务是真正的 TPL 任务。当你await
在时Consumer()
,那么其余的Consumer()
方法实际上是作为从完成Task
返回的继续安装的Func(int n)
。
- CTP 编译器转换异步方法的方式导致在实际返回之前
return
映射到SetResult(...)
调用。SetResult(...)
归结为对TaskCompletionSource<>.TrySetResult
.
TaskCompletionSource<>.TrySetResult
表示 TPL 任务完成。立即使其延续“有时”发生。这个“有时”可能意味着在另一个线程上,或者在某些情况下,TPL 很聪明并说“嗯,我现在不妨在同一个线程上调用它”。
- 在 finally 运行之前,最重要
Task
的 for在技术上已“完成”。Func(int n)
这意味着等待异步方法的代码可能在并行线程中运行,甚至在 finally 块之前运行。
考虑到总体Task
应该表示方法的异步状态,基本上它不应该被标记为已完成,直到至少所有用户提供的代码都已按照语言设计执行。我将与 Anders、语言设计团队和编译器开发人员一起讨论这个问题。
表现范围/严重性:
在 WPF 或 WinForms 情况下,您通常不会对此感到厌烦,因为您有某种托管消息循环正在进行。原因是await
onTask
实现遵循SynchronizationContext
. 这会导致异步继续在预先存在的消息循环上排队,以便在同一线程上运行。您可以通过更改代码以Consumer()
按以下方式运行来验证这一点:
DispatcherFrame frame = new DispatcherFrame(exitWhenRequested: true);
Action asyncAction = async () => {
await Consumer();
frame.Continue = false;
};
Dispatcher.CurrentDispatcher.BeginInvoke(asyncAction);
Dispatcher.PushFrame(frame);
一旦在 WPF 消息循环的上下文中运行,输出就会如您所愿:
Consumer: before await #1
Func: Begin #1
Func: End #1
Func: Finally #1
Consumer: after await #1
Consumer: before await #2
Func: Begin #2
Func: End #2
Func: Finally #2
Consumer: after await #2
Consumer: after the loop
After the wait
解决方法:
唉,解决方法意味着将代码更改为不使用块return
内的语句try/finally
。我知道这确实意味着您在代码流中失去了很多优雅。您可以使用异步辅助方法或辅助 lambda 来解决此问题。就个人而言,我更喜欢 helper-lambdas,因为它会自动关闭包含方法中的局部变量/参数,并使您的相关代码更接近。
辅助 Lambda 方法:
static async Task<int> Func( int n )
{
int result;
try
{
Func<Task<int>> helperLambda = async() => {
Console.WriteLine( " Func: Begin #{0}", n );
await TaskEx.Delay( 100 );
Console.WriteLine( " Func: End #{0}", n );
return 0;
};
result = await helperLambda();
}
finally
{
Console.WriteLine( " Func: Finally #{0}", n );
}
// since Func(...)'s return statement is outside the try/finally,
// the finally body is certain to execute first, even in face of this bug.
return result;
}
辅助方法方法:
static async Task<int> Func(int n)
{
int result;
try
{
result = await HelperMethod(n);
}
finally
{
Console.WriteLine(" Func: Finally #{0}", n);
}
// since Func(...)'s return statement is outside the try/finally,
// the finally body is certain to execute first, even in face of this bug.
return result;
}
static async Task<int> HelperMethod(int n)
{
Console.WriteLine(" Func: Begin #{0}", n);
await TaskEx.Delay(100);
Console.WriteLine(" Func: End #{0}", n);
return 0;
}
作为一个无耻的插件:我们正在微软的语言领域招聘,并且一直在寻找优秀的人才。此处的博客条目包含未平仓职位的完整列表 :)