无法使用捕获的GUI上下文继续操作,但是为什么会出现死锁?

时间:2019-04-21 10:18:03

标签: c# asynchronous async-await

我想知道为什么在以下情况下会死锁,在这种情况下无法使用捕获的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上下文,从而导致死锁。

1 个答案:

答案 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>,但是思想是相同的。