【问题标题】:One-line async method SynchronizationContext一行异步方法 SynchronizationContext
【发布时间】:2019-11-24 09:00:28
【问题描述】:

我准备了 WinForms 应用程序来测试单行异步方法是否会导致死锁。 button1_Click 事件等待由单行异步代理方法等待的GetZero 任务。但是,它会导致死锁。为什么?我读过await 完成后,单行异步方法不需要继续执行任何操作,因此没有委托发布到消息泵导致死锁。

作为参考,button2_Click 事件在没有代理调用者的情况下等待任务GetZero 的结果,并且应用程序工作正常。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var zero = ProxyCallery().Result;
        label1.Text += $"{zero}";
    }

    private void button2_Click(object sender, EventArgs e)
    {
        var zero = GetZero().Result;
        label1.Text += $"{zero}";
    }

    private async Task<int> ProxyCallery()
    {
        return await GetZero();
    }

    private async Task<int> GetZero()
    {
        await Task.Delay(100).ConfigureAwait(false);

        return await Task.FromResult(0);
    }
}

为什么button1_Click会导致死锁?

【问题讨论】:

  • 尝试在 ProxyCallery 中的 await 表达式上调用 ConfigureAwait(false)。我依稀记得ConfigureAwaits 必须沿着调用链保持一致。
  • Don't Block on Async Code。见Preventing the Deadlock部分。

标签: c# winforms deadlock synchronizationcontext


【解决方案1】:

await Task.Delay(100).ConfigureAwait(false); 仅为该调用配置等待。它不会影响可能依赖于该特定等待的等待,例如 ProxyCallery() 方法中的 await GetZero()

后一个等待仍然需要在 UI 线程中继续,您已使用 ProxyCallery().Result 阻止了该线程。因此陷入僵局。

我读过单行异步方法在等待完成后不需要继续任何事情,因此没有委托发布到消息泵导致死锁。

我不知道你是从哪里读到的,但这是错误的。编译器不会尝试优化“尾等待”。实际上,即使方法中的最后一件事是await,仍然有代码要在延续上执行。至少,解开任何异常,但也将延续传播到 async 方法所代表的 Task

因此,与在方法中的任何其他位置发现的 await 语句相比,结束方法的 await 语句在死锁可能性或异步执行的任何其他方面完全没有区别。

【讨论】:

  • 那么为什么不需要在 GetZero 方法中配置第二次等待呢? return await Task.FromResult(0);从你所说的这个等待也需要在相同的同步上下文中继续,那么为什么它不会导致死锁?
  • 因为等待直到之后前一个配置的等待完成后才会发生。这与您在ProxyCallery() 中询问的等待不同,后者发生在之前 之前配置的等待已完成。重要的是您在等待时所处的同步上下文,而不是在等待的事物启动时。由于配置的等待还没有“回来”,同步上下文仍然是异步方法最初产生时的状态。在本例中,这就是 UI 线程。
  • 如果我错了,请纠正我 - return await Task.FromResult(0); 不会导致死锁,因为它是从不同于“UI 线程”的上下文启动的,因为之前的任务是 ConfigureAwait?具体来说,它是在之前的 await? 的上下文中启动的
  • "return await Task.FromResult(0); 不会导致死锁,因为它是从不同的上下文启动的" -- 是的,但大多数情况下不会。如果该任务是真正的异步任务,那么您的推理将是正确的。但是,Task.FromResult(0) 返回一个已经完成的任务,因此特定的await 同步完成。 IE。它立即返回而不产生线程,因此如果线程产生,线程会做什么都没有关系,不会发生死锁。
【解决方案2】:

您的ProxyCallery 实际上是两行,因为在异步操作之后有 延续:

private async Task<int> ProxyCallery()
{
    var zero = await GetZero();
    return zero; // <-- continuation!
}

继续是返回任务的结果

目前,上述延续与Task.Delay任务的延续不在同一个同步上下文中,这就是导致死锁的原因。

您创建的每个任务都应该在同一个同步上下文中!

private async Task<int> ProxyCallery()
{
    var zero = await GetZero().ConfigureAwait(false);
    return zero;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-09
    • 1970-01-01
    相关资源
    最近更新 更多