以下是代码:
static class AsyncFinally
{
static async Task<int> Func( int n )
{
try
{
Console.WriteLine( " Func: Begin #{0}", n );
await TaskEx.Delay( 100 );
Console.WriteLine( " Func: End #{0}", n );
return 0;
}
finally
{
Console.WriteLine( " Func: Finally #{0}", n );
}
}
static async Task Consumer()
{
for ( int i = 1; i <= 2; i++ )
{
Console.WriteLine( "Consumer: before await #{0}", i );
int u = await Func( i );
Console.WriteLine( "Consumer: after await #{0}", i );
}
Console.WriteLine( "Consumer: after the loop" );
}
public static void AsyncTest()
{
Task t = TaskEx.RunEx( Consumer );
t.Wait();
Console.WriteLine( "After the wait" );
}
}
这是输出:
Consumer: before await #1
Func: Begin #1
Func: End #1
Consumer: after await #1
Consumer: before await #2
Func: Begin #2
Func: Finally #1
Func: End #2
Consumer: after await #2
Consumer: after the loop
Func: Finally #2
After the wait
正如您所看到的,finally块在很多后执行,然后您会期望。
有任何解决方法吗?
提前致谢!
答案 0 :(得分:13)
这是一个很好的捕获 - 我同意这里的CTP实际上存在一个错误。我挖了它,这就是发生了什么:
这是异步编译器转换的CTP实现的组合,以及.NET 4.0+的TPL(任务并行库)的现有行为。以下是正在发挥作用的因素:
Task
方法的首要Func(int n)
是一个真正的TPL任务。当您await
中的Consumer()
时,Consumer()
方法的其余部分实际上是作为从Task
返回的Func(int n)
完成后的继续安装的。 return
在实际返回之前映射到SetResult(...)
调用。 SetResult(...)
归结为对TaskCompletionSource<>.TrySetResult
的调用。TaskCompletionSource<>.TrySetResult
表示完成了TPL任务。立即使其延续“有时”发生。这个“某个时间”可能意味着在另一个线程上,或者在某些情况下TPL是聪明的并且说“嗯,我不妨现在在同一个线程上调用它”。Task
的首要Func(int n)
在最终运行之前就在技术上变为“已完成”。这意味着等待异步方法的代码可能在并行线程中运行,甚至在finally块之前运行。考虑到总体Task
应该代表该方法的异步状态,从根本上说它不应该被标记为已完成,直到至少所有用户提供的代码都按照语言设计执行。我将与Anders,语言设计团队和编译器开发人员讨论这个问题。
表现/严重程度的范围:
在WPF或WinForms情况下,您通常不会对此有所了解,因为您正在进行某种托管消息循环。原因是await
实施Task
遵循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
语句。我知道这真的意味着你在代码流中失去了很多优雅。您可以使用异步辅助方法或帮助程序lambdas来解决此问题。就个人而言,我更喜欢helper-lambdas,因为它会自动关闭包含方法的locals /参数,并使相关代码保持更近。
Helper 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;
}
作为一个无耻的插件:我们正在微软的语言空间招聘,并一直在寻找优秀的人才。博客条目here以及未结头寸的完整列表:)
答案 1 :(得分:2)
请考虑Theo Yaung的answer。
我不熟悉async / await,但在阅读之后: Visual Studio Async CTP Overview
并阅读您的代码,我在await
函数中看到Func(int n)
,这意味着从{i>之后的<{i> await
关键字直到结束时函数稍后将作为代理执行。
所以我的猜测(这是一个没有受过教育的猜测)是Func:Begin
和Func:End
可能会在不同的“上下文”(线程?)中执行,即,异步。
因此,int u = await Func( i );
中的Consumer
行将在await
中的代码Func
到达时继续执行。所以很有可能:
Consumer: before await #1
Func: Begin #1
Consumer: after await #1
Consumer: before await #2
Func: Begin #2
Consumer: after await #2
Consumer: after the loop
Func: End #1 // Can appear at any moment AFTER "after await #1"
// but before "After the wait"
Func: Finally #1 // will be AFTER "End #1" but before "After the wait"
Func: End #2 // Can appear at any moment AFTER "after await #2"
// but before "After the wait"
Func: Finally #2 // will be AFTER "End #2" but before "After the wait"
After the wait // will appear AFTER the end of all the Tasks
Func: End
和Func: Finally
可以显示在日志中的任何位置,唯一的限制是Func: End #X
将出现在其关联的Func: Finally #X
之前,并且两者都应该出现在After the wait
之前。
正如Henk Holterman所解释的那样(有点突然),你在await
体内放置Func
的事实意味着之后的所有事情都会在之后执行。< / p>
没有解决方法,by design
您在await
和Begin
End
之间放置Func
。
只是我未受过教育的2欧元。