我有一个关于在C#中包装异步方法的奇怪行为。
很难删除我的代码中有这种奇怪行为的部分,所以我做了一个测试项目来调查行为,这就是我发现的。
我有一个测试类,它有一个async方法,它只是另一个异步方法的包装器:(在我的原始代码中,它是一个包含2个对象的包装类,它有包装方法)
public class Test
{
public async Task Delay()
{
await Task.Delay(1000);
}
}
在我的测试程序中,我从异步事件处理程序运行以下代码:(在我的情况下是Loaded事件,因为我使用的是WPF窗口)
var test = new Test();
await Task.Delay(1000);
await test.Delay();
Task.Delay(1000).Wait();
test.Delay().Wait();
一直很好,直到最后一行,永远不会回来。
然后我尝试将测试类更改为以下内容,最后一行有效:
public class Test
{
public Task Delay()
{
return Task.Delay(1000);
}
}
我的问题是为什么第一种情况不起作用?
答案 0 :(得分:5)
我详细描述了这个死锁场景on my blog和MSDN article。
默认情况下,当您await
任务时,它会捕获当前的“上下文”(SynchronizationContext.Current
,除非它是null
,在这种情况下它会捕获TaskScheduler.Current
) 。当async
方法恢复时,它将在该上下文中恢复。
在您的情况下,将捕获UI上下文,并且您的async
方法在延迟完成后尝试在UI线程上继续。但是,它无法继续执行,因为UI线程被阻止等待任务完成。
最好的解决方案是一直使用async
“;换句话说,不要阻止async
代码。
答案 1 :(得分:4)
通过默认构建await
关键字,以尽可能多地保留在原始线程上运行的原始代码(或者,至少在相同的“上下文”中)作为在await
之前运行的代码,例如,如果您之前在线程池中运行,则任何线程池线程都会这样做。
正在发生的事情是,所有原始代码在运行时都希望在UI线程上运行。
但是您阻止了Wait
中的UI线程,因此永远无法在async
方法中运行其余代码,因此完成了外部Task
。即在Task.Delay
任务完成后,只需要在您的方法中运行这一小段代码:
public class Test
{
public async Task Delay()
{
await Task.Delay(1000);
//<-- This "Code" needs to run before my Task is completed
}
}
您可以使用ConfigureAwait(continueOnCapturedContext)
请求不会发生这种情况:
continueOnCapturedContext
true 尝试将继续编组回到捕获的原始上下文;否则, false 。
请参阅Eric Lippert撰写的旧版blog post,从async
新版开始:
方法上的“async”修饰符并不意味着“此方法会自动安排在工作线程上异步运行”。这意味着相反;它意味着“此方法包含涉及等待异步操作的控制流,因此将由编译器重写为连续传递样式,以确保异步操作可以在正确的位置恢复此方法。”异步方法的重点你尽可能地留在当前的线程上。