我想知道为什么在以下情况下会死锁,在这种情况下无法使用捕获的GUI上下文继续进行操作。
public Form1()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = true;
}
async Task DelayAsync()
{
// GUI context is captured here (right before the following await)
await Task.Delay(3000);//.ConfigureAwait(false);
// As no code follows the preceding await, there is no continuation that uses the captured GUI context.
}
private async void Button1_Click(object sender, EventArgs e)
{
Task t = DelayAsync();
t.Wait();
}
我知道死锁可以通过以下任一方法解决
await Task.Delay(3000).ConfigureAwait(false);
或t.Wait();
替换await t;
。但这不是问题。问题是
为什么在没有继续使用捕获的GUI上下文的情况下会出现死锁?在我的心智模型中,如果存在延续,那么它将使用捕获的GUI上下文,从而导致死锁。
答案 0 :(得分:4)
TL; DR: async
适用于等待者,而不适用于任务。因此,在方法末尾需要额外的逻辑来将等待者的状态转换为任务。
您没有连续性的假设是错误的。如果您刚返回任务,那将是真的:
Task DelayAsync()
{
return Task.Delay(3000);
}
但是,将方法标记为async
会使事情变得更加复杂。 async
方法的一个重要属性是其处理异常的方式。例如,考虑那些方法:
Task NoAsync()
{
throw new Exception();
}
async Task Async()
{
throw new Exception();
}
现在,如果调用它们会发生什么?
var task1 = NoAsync(); // Throws an exception
var task2 = Async(); // Returns a faulted task
区别在于异步版本将异常包装在返回的任务中。
与我们的案件有什么关系?
当您使用await
方法时,编译器实际上会在等待的对象上调用GetAwaiter()
。等待者定义了3个成员:
IsCompleted
属性OnCompleted
方法GetResult
方法如您所见,没有成员直接返回异常。如何知道侍应生是否过失?要知道这一点,您需要调用GetResult
方法,该方法将引发异常。
回到您的示例:
async Task DelayAsync()
{
await Task.Delay(3000);
}
如果Task.Delay
引发异常,则async
机器需要将返回任务的状态设置为故障。要知道Task.Delay
是否引发异常,它需要在GetResult
完成之后在等待者上调用Task.Delay
。因此,您可以继续执行,尽管在查看代码时看不到它。在后台,异步方法看起来像:
Task DelayAsync()
{
var tcs = new TaskCompletionSource<object>();
try
{
var awaiter = Task.Delay(3000).GetAwaiter();
awaiter.OnCompleted(() =>
{
// This is the continuation that causes your deadlock
try
{
awaiter.GetResult();
tcs.SetResult(null);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
}
catch (Exception ex)
{
tcs.SetException(ex);
}
return tcs.Task;
}
实际代码更复杂,并且使用AsyncTaskMethodBuilder<T>
代替TaskCompletionSource<T>
,但是思想是相同的。