【问题标题】:Usage of ConfigureAwait in .NET在 .NET 中使用 ConfigureAwait
【发布时间】:2020-10-22 04:35:28
【问题描述】:

我在各个地方(包括 SO 问题)都阅读过有关 ConfigureAwait 的信息,以下是我的结论:

  • ConfigureAwait(true):在运行 await 之前的代码所在的同一线程上运行其余代码。
  • ConfigureAwait(false):在运行等待代码的同一线程上运行其余代码。
  • 如果 await 后面是访问 UI 的代码,则该任务应附加 .ConfigureAwait(true)。否则,由于另一个线程访问 UI 元素,将发生 InvalidOperationException。

我的问题是:

  1. 我的结论正确吗?
  2. ConfigureAwait(false) 何时会提高性能,何时不会?
  3. 如果为 GUI 应用程序编写,但下一行不访问 UI 元素。我应该使用 ConfigureAwait(false) 还是 ConfigureAwait(true) ?

【问题讨论】:

  • 我不建议使用 SO 作为有关 async/await 的主要信息来源。自认为知道它是如何工作的人在这里有很多错误信息。使用 Stephen Toub、Stephen Cleary、Jon Skeet 的文档和著作。但要违背我自己的建议,您可能会找到one of my blog posts helpful
  • Are my conclusions correct - 没有。异步与线程无关。 ConfigureAwait 与某个线程的使用没有直接对应关系。参见例如stackoverflow.com/q/46094134/11683.
  • 您必须定义“性能”的含义。异步代码实际上比相同的非异步代码性能稍差。主要优势在于可扩展性和并行性,尤其是 I/O 绑定代码。
  • @Youssef13 Async 关注的是同步上下文,而不是线程。使用或不使用单独的线程是一个实现细节,除非另有明确说明(例如使用Task.Run())。 ConfigureAwait(false) premits 在不捕获同步上下文的情况下恢复,而不是需要它。对于某些同步上下文(例如,在 Winforms 应用程序中使用的上下文),“在同一上下文中恢复”意味着“在 UI 线程上运行”。对于其他情况,它可能并不意味着,或者可能根本没有明显的影响。
  • @Youssef13 是的,你找到了 Stephen Toub。他谈到了可能不适用于您的“热路径”,性能增益可能太小而无法衡量(只是一个没有看到您的代码的假设)。不要将ConfigureAwait(false) 用于“上下文感知”代码,例如完成后必须在 UI 线程上执行的代码。另一个示例是在 ASP.NET WebForms 应用程序中访问 HttpContext。我发布的链接指向一个探索这些事情的 WPF 应用程序,包括在使用带有草率的库代码的 ConfigureAwait(false) 时如何仍然死锁。

标签: c# .net asynchronous async-await configureawait


【解决方案1】:

ConfigureAwait(false) 可能会在没有很多可用的工作线程并且它需要等待的线程一直很忙的情况下提高性能。

ConfigureAwait(false) 建议在不需要返回相同 SynchronizationContext(通常与线程链接)的任何地方使用,尤其是在内部等待某些内容的库中:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f

ConfigureAwait(true)(这是默认值)在您需要相同的上下文时需要,但在某些情况下也可能导致死锁。

考虑这段代码:

void Main()
{
    // creating a windows form attaches a synchronization context to the current thread
    new System.Windows.Forms.Form();
    var task = DoSth();
    Console.WriteLine(task.Result);
}

async Task<int> DoSth()
{
    await Task.Delay(1000);
    return 1;
}

在这个例子中,由于没有等待任务 DoSth,主 UI 线程被等待 task.Result 阻塞 - 同时 DoSth 被阻塞,因为它想在延迟后回到 UI 线程。这将导致死锁,并且这段代码永远不会执行到最后。在这种情况下添加.ConfigureAwait(false) 即可解决问题。

【讨论】:

  • 说在库中推荐 ConfigureAwait(true) 让我更加困惑。
  • @Youssef13:这是因为您无法控制开发人员是否会以正确的方式使用您的库。通过在库内部调用中添加.ConfigureAwait(false),当开发人员希望以同步方式(使用.Result)使用您的库时,您可以防止潜在的死锁。当然,任务不应该以这种方式使用,但在用户眼中,造成问题的将是你的库。
  • @Adassko,您在回答中声明了 ConfigureAwait(true)。也许是错字?
  • @Youssef13 是的,我想写关于 (true) 的内容,但删除了它并开始输入关于 (false) 的内容。我的错误
  • 推荐.ConfigureAwait(false) 作为解决死锁的方法是not good
【解决方案2】:

在应用程序代码中使用ConfigureAwait(false) 通常不会以任何有意义的方式提高应用程序的性能,因为通常您不会在应用程序代码的循环中使用await。例如,假设您的应用程序有一个按钮,并且每次用户单击该按钮时都会启动一个异步操作,并且该异步操作包含一个 await。通过在 await 之后键入 22 个字符 .ConfigureAwait(false),您已经失去了可比的生命时间,您可以希望从 10 个每分钟点击此按钮一次的用户中节省时间,每天 8 小时,持续 20 年每个(总共约 35,000,000 次上下文切换 = 几秒钟的 CPU 处理时间)。

在考虑到您需要考虑是否可以安全地包含此配置之前(取决于延续是否包含与 UI 相关的代码),您需要时间来重新确认您之前的评估每次您必须维护/修改代码的时间,以及在您的评估错误的情况下您将失去调试的时间。

另一方面,如果您的 Button_Click 处理程序包含如下代码:

private async void Button_Click(object sender, EventArgs e)
{
    var client = new WebClient();
    using var stream = await client.OpenReadTaskAsync("someUrl");
    var buffer = new byte[1024];
    while ((await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
    {
        //...
    }
}

...那么请务必将额外的时间花在ConfigureAwait(false) ReadAsync 任务上。还要考虑通过将流读取部分移动到单独的异步方法来重构代码,以便您可以安全地访问Button_Click 处理程序内的任何地方的 UI 元素,而不会被不属于这一层的技术分心应用程序。

【讨论】:

    【解决方案3】:

    更直接地回答您的问题:

    ConfigureAwait(true):在运行 await 之前的代码所在的同一线程上运行其余代码。

    不一定是同一个线程,而是同一个同步上下文。同步上下文可以决定如何运行代码。在 UI 应用程序中,它将是同一个线程。在 ASP.NET 中,它可能不是同一个线程,但您将拥有可用的 HttpContext,就像您之前所做的那样。

    ConfigureAwait(false):在运行等待代码的同一线程上运行其余代码。

    这是不正确的。 ConfigureAwait(false) 告诉它它不需要上下文,因此代码可以在任何地方运行。它可以是运行它的任何线程。

    如果 await 后面是访问 UI 的代码,则该任务应附加.ConfigureAwait(true)。否则,会因为另一个线程访问 UI 元素而引发 InvalidOperationException。

    它“应该附加.ConfigureAwait(true)”是不正确的。 ConfigureAwait(true) 是默认值。所以如果这是你想要的,你不需要指定它。

    1. ConfigureAwait(false) 何时会提高性能,何时不会?

    返回同步上下文可能需要一些时间,因为它可能必须等待其他东西完成运行。实际上,这种情况很少发生,或者等待时间非常短,以至于您永远都不会注意到。

    1. 如果为 GUI 应用程序编写,但下一行不访问 UI 元素。我应该使用 ConfigureAwait(false) 还是 ConfigureAwait(true) ?

    可以使用ConfigureAwait(false),但我建议您不要使用,原因如下:

    1. 我怀疑您会注意到任何性能改进。
    2. 它可以引入您可能没想到的并行性。如果你使用ConfigureAwait(false),延续可以在任何线程上运行,所以如果你访问非线程安全的对象,你可能会遇到问题。出现这些问题并不常见,但可能会发生。
    3. 您(或维护此代码的其他人)可能会添加稍后与 UI 交互的代码,并且将引发异常。希望ConfigureAwait(false) 很容易被发现(它可能与引发异常的方法不同)并且您/他们知道它的作用。

    我发现根本不使用ConfigureAwait(false) 会更容易(在库中除外)。用 ConfigureAwait FAQ 中的 Stephen Toub(微软员工)的话来说:

    在编写应用程序时,您通常需要默认行为(这就是为什么它是默认行为)。

    【讨论】:

    • 嗨加布里埃尔,我很高兴你还活着!你从 SO 消失了一年左右。我想念草原上那头平静的奶牛的形象。 ?
    • @TheodorZoulias 嘿!我很惊讶你注意到了。我一直在潜伏,但过去的一年忙碌而压力大,所以我没有花时间回答任何问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-06-24
    • 2023-03-30
    • 1970-01-01
    • 1970-01-01
    • 2013-09-11
    • 1970-01-01
    相关资源
    最近更新 更多