混合使用async/await和Result

10

在提出问题之前,我想先说几句话:

  1. 我已经阅读了几篇 Stack Overflow 的文章,指出你不应该这样做(例如如何安全地混合同步和异步代码)。
  2. 我阅读了异步编程最佳实践,这篇文章也说你不应该这样做。

所以我知道这不是最佳实践,也不需要别人告诉我这一点。这更像是一个“为什么会这样工作”的问题。

那么问题来了:

我编写了一个小的图形用户界面应用程序,其中有2个按钮和一个状态标签。其中一个按钮会100%复现同步和异步死锁问题。另一个按钮调用相同的异步方法,但它被包装在一个任务中,这能够正常运行。我知道这不是一个好的编码实践,但我想了解为什么它没有相同的死锁问题。以下是代码:

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

    private async Task<string> DelayAsync()
    {
        await Task.Delay(1000);
        return "Done";
    }

    private void buttonDeadlock_Click(object sender, EventArgs e)
    {
        labelStatus.Text = "Status: Running";

        // causes a deadlock because of mixing sync and async code
        var result = DelayAsync().Result;
        // never gets here
        labelStatus.Text = "Status: " + result;
    }

    private void buttonWorking_Click(object sender, EventArgs e)
    {
        labelStatus.Text = "Status: Running";
        string result = null;

        // still technically mixes sync and async, but works, why?
        result = Task.Run(async () =>
        {
            return await DelayAsync();
        }).Result;

        labelStatus.Text = "Status: " + result;
    }
}
2个回答

12
它之所以有效,是因为 buttonWorking_Click 异步代码(包括 DelayAsync 和传递给 Task.Runasync lambda)没有当前的 SynchronizationContext,而 buttonDeadlock_Click 异步代码(DelayAsync)有。您可以通过在调试器中运行并观察 SynchronizationContext.Current 来观察差异。
我在我的博客文章《不要阻塞异步代码》中解释了死锁情况背后的细节。

10

场景一:你坐在桌子前,有一个收件箱,它是空的。突然一张纸片出现在你的收件箱里描述了一个任务,你跳起来开始四处奔波去完成任务。但是这个任务是什么?它让你做以下几件事情:

  • 更改白板上的内容为“正在进行” - 好的,你完成了。
  • 将闹钟设定为1小时后再响 - 好的,你完成了。
  • 创建一张新的纸片,写上“当闹钟响起时,在白板上写下DONE这个词”。把这张纸放在你的收件箱里 - 你完成了。
  • 除了在白板上写下DONE这个词之外,不要做任何其他事情。
  • 回到你的桌子前等待下一个任务出现在收件箱中。

这个流程阻碍你完成工作,因为最后两个步骤的顺序是错误的。

场景二:你坐在桌子前,有一个收件箱,它是空的。突然一张纸片出现在你的收件箱里描述了一个任务,你跳起来开始四处奔波去完成任务。但是这个任务是什么?它让你做以下几件事情:

  • 更改白板上的内容为“正在进行” - 好的,你完成了。
  • 把这张纸片交给下一个小隔间里的Debbie - 好的,你完成了。
  • 在有人告诉你子任务已经完成之前不要做任何事情。
  • 当那件事发生时,在你的白板上写下DONE这个词。
  • 回到你的桌子前。

你给Debbie的那张纸上写了什么?它写着:

  • 将闹钟设定为1小时后再响 - 好的,她完成了。
  • 当闹钟响起时,放一张纸片在你的收件箱里,上面写着告诉Middas你已经完成了。

这种工作流程仍然很糟糕,因为(1)当你等待Debbie的闹钟响起时,你会坐在那里无所事事,(2)当你可以让一个人完成所有工作时,你浪费了两个工人的时间。工人很昂贵。

但这种工作流程并不会阻止你最终完成工作。它不会死锁,因为你不是在等待自己将来要做的工作,而是在等待别人去做这项工作。

(我注意到这并不是您程序中正在发生的情况的精确类比,但足以传达这个想法。)


我很感激你的例子,它有助于更好地理解为什么在这种情况下会发生死锁。 - Middas
3
这正是我为什么订阅Eric的StackOverflow RSS订阅源的原因。即使你已经知道答案,你也肯定会得到额外的见解和/或解释事物给同事的好方法。 - Graham Clark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接