没有继续使用捕获的GUI上下文,但为什么会出现死锁?

问题描述 投票:0回答:1

我想知道为什么在以下情况下存在死锁,其中没有使用捕获的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();
}

Edit:

我知道死锁可以解决

  • 使用await Task.Delay(3000).ConfigureAwait(false);
  • t.Wait();取代await t;

但这不是问题。问题是

为什么在没有继续使用捕获的GUI上下文时会出现死锁?在我的心智模型中,如果有延续,那么它将使用捕获的GUI上下文,因此它将导致死锁。

c# asynchronous async-await
1个回答
4
投票

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>,但想法是一样的。

© www.soinside.com 2019 - 2024. All rights reserved.