我想知道为什么在以下情况下存在死锁,其中没有使用捕获的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上下文,因此它将导致死锁。
TL; DR:async
与等待者合作,而非任务。因此,在方法结束时需要额外的逻辑来将awaiter的状态转换为任务。
你没有延续的假设是错误的。如果您刚刚返回任务,那将是真的:
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
方法如您所见,没有成员直接返回异常。如何知道awaiter是否出现故障?要知道这一点,你需要调用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>
,但想法是一样的。