【问题标题】:Can using async-await give you any performance benefits?使用 async-await 可以为您带来任何性能优势吗?
【发布时间】:2016-08-09 13:41:35
【问题描述】:

每当我读到async-await 时,用例示例总是其中有一个您不想冻结的 UI。要么所有编程书籍/教程都相同,要么 UI 阻塞是 async-await 作为开发人员应该知道的唯一情况。

是否有任何示例说明如何使用async-await 在算法中获得性能优势?就像让我们回答任何经典的编程面试问题:

  • 在二叉树中找到最近的共同祖先
  • 给定a[0]a[1]、...、a[n-1] 代表以 10 为基数的数字,找出使用相同数字的下一个最大数字
  • 求两个已排序数组的中位数(即合并它们时的中位数)
  • 给定一组数字12、...、n,其中一个数字缺失,找出缺失的数字
  • 找出数组中最大的 2 个数字

有什么方法可以让那些使用async-await 的人获得性能优势?如果是这样,如果您只有 1 个处理器怎么办?那么你的机器不是只是在任务之间分配时间,而不是真正同时执行它们吗?

【问题讨论】:

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


    【解决方案1】:

    this interview, Eric Lippert compared async await with a cook making breakfast。它对我理解 async-await 的好处有很大帮助。在中间某处搜索“async-await”

    假设厨师必须做早餐。他得烤些面包,煮些鸡蛋,也许还要泡些茶?

    方法一:同步。由一个线程执行。你开始烤面包。等到面包烤好。取出面包。开始煮水,等到水沸腾并插入鸡蛋。等到鸡蛋准备好并取出鸡蛋。开始烧水泡茶。等水烧开再泡茶。

    你会看到所有的等待。当线程等待时,它可以做其他事情。

    方法 2:异步等待,仍然是一个线程 你开始烤面包。烤面包时,您开始为鸡蛋和茶煮水。然后你开始等待。当三个任务中的任何一个完成后,您将执行任务的第二部分,具体取决于哪个任务首先完成。因此,如果鸡蛋的水先沸腾,则您将鸡蛋煮熟,然后再次等待任何任务完成。

    在这个描述中,只有一个人(你)在做所有的事情。只涉及一个线程。好消息是,因为只有一个线程在做这些事情,所以代码对读者来说看起来非常同步,并且不需要让你的变量线程安全。

    很容易看出,这样您的早餐将在更短的时间内准备好(而且您的面包仍然很热!)。在计算机生活中,当您的线程必须等待另一个进程完成时,这些事情就会发生,例如将文件写入磁盘,从数据库或互联网获取信息。这些通常是会看到函数的异步版本的函数类型:WriteWriteAsyncReadReadAsync

    补充:经过其他地方其他用户的一些评论,以及一些测试,我发现实际上可以是任何线程在等待之后继续你的工作。这个其他线程具有相同的“上下文”,因此可以像原来的线程一样工作。

    方法3:在泡茶的同时聘请厨师烤面包和煮鸡蛋:真正的异步。多个线程 这是最昂贵的选项,因为它涉及创建单独的线程。在做早餐的例子中,这可能不会大大加快这个过程,因为在相对较大的过程中你什么都不做。但是,例如,如果您还需要切西红柿,让厨师(单独的线程)执行此操作,而您使用 async-await 执行其他操作可能会很方便。当然,您要做的其中一项等待就是等待厨师完成他的切片。

    另一篇解释很多的文章是Async and Await,由非常乐于助人的 Stephen Cleary 撰写。

    【讨论】:

    • 非常感谢您分享这个,它对我理解这个概念很有帮助。
    【解决方案2】:

    每当我读到 async-await 时,用例示例总是存在您不想冻结的 UI。

    这是async 最常见的用例。另一种是在服务器端应用程序中,async 可以提高 Web 服务器的可扩展性。

    是否有任何示例说明如何使用 async-await 在算法中获得性能优势?

    没有。

    如果您想进行并行处理,可以使用任务并行库。并行处理是使用多个线程,在系统中的多个内核之间划分算法的一部分。并行处理是并发的一种形式(同时做多件事)。

    异步代码完全不同。异步代码的要点是在操作进行时使用当前线程。异步代码通常受 I/O 限制或基于事件(如计时器)。异步代码是另一种形式的并发。

    我的博客上有一个async intro,还有一个how async doesn't use threads 上的帖子。

    请注意,任务并行库使用的任务可以安排到线程上并执行代码。基于任务的异步模式使用的任务没有代码,也不会“执行”。尽管这两种类型的任务都由相同的类型(Task)表示,但它们的创建和使用完全不同;我在我的博客上更详细地描述了这些Delegate Tasks and Promise Tasks

    【讨论】:

    • 嗨,Stephen,您是否有任何文章解释了async 如何提高 Web 服务器的可扩展性?
    • @Hendry 只需以任何 篇以网络服务器为目标的论文讨论异步 I/O 相对于多线程和阻塞的优势,所有关键点基本相同。 IE。检查stackoverflow.com/questions/14795145/… 并用“任务/延续”和“调度程序”在心理上替换“回调”、“队列”和“事件”
    • 感谢@quetzalcoatl,所以我假设是对的,这是因为线程现在通过不等待 I/O 绑定操作而得到充分利用,通过最小化线程池来提高可伸缩性由于线程池饥饿而导致大小增加?基本不浪费资源。
    • @Hendry:如果我理解你的话——是的。更少的线程可以处理相同数量的请求,因为所有将在 I/O 上休眠的线程现在都将时间花在其他工作块上 => 使用相同的 N 个线程可以完成更多工作。线程池大小是一回事,但处理线程通常很昂贵。不仅创建/销毁(通过池化有所缓解)而且线程切换(使线程进入强制睡眠,唤醒另一个线程)比让线程从 jobA 跳转到 jobB 更昂贵,尽管任务调度仍然浪费了一些时间/(de)排队/等。
    • @Hendry:对。你可以说async 允许你最大限度地利用你的线程池。
    【解决方案3】:

    简而言之,一般情况下 - 不,通常不会。但这需要多说几句,因为“性能”可以有多种理解。

    仅当“作业”受 I/O 限制时,异步/等待“节省时间”。任何将它应用到受 CPU 限制的作业都会带来一些性能损失。那是因为如果你有一些计算需要在你的 CPU 上花费 10 秒,那么添加 async/await - 即:任务创建、调度和同步 - 只会在你仍然需要刻录的 10 秒中增加 X 额外时间你的 CPU 来完成工作。类似于阿姆达尔定律的概念。不是真的,但很接近。

    但是,有一些'但是..'。

    首先,由于引入异步/等待而导致的性能损失并没有那么大。 (特别是如果你小心不要过度)。

    其次,由于 async/await 允许您更轻松地编写 I/O 交错代码,您可能会注意到在您懒得 (:) ) 无法执行的地方删除 I/O 等待时间的新机会否则,或者在没有 async/await 语法优点的情况下会使代码难以遵循的地方。例如,围绕网络请求拆分代码是相当明显的事情,但您可能会注意到,您也可以在编写 CSV 文件或读取配置文件等的少数地方升级一些文件 i/o。不过,请注意这里的收益不会归功于 async/await - 这将归功于重写处理文件 i/o 的代码。你也可以在没有 async/await 的情况下做到这一点。

    第三,由于某些 i/o 操作更容易,您可能会注意到将 CPU 密集型工作卸载到另一个服务或机器要容易得多,这也可以提高您的感知性能(更短的“挂钟”时间),但整体资源消耗会上升:增加另一台机器,花时间在网络操作上等等。

    第四:用户界面。你真的不想冷冻它。将 I/O 密集型和 CPU 密集型作业包装在 Tasks 中并在它们上进行 async/await 并保持 UI 响应非常容易。这就是为什么你到处都提到它的原因。然而,虽然 I/O 绑定的操作理想情况下应该异步到叶子节点以消除所有冗长 I/O 上的尽可能多的空闲等待时间,但 CPU 绑定的作业不需要拆分或异步化,而不仅仅是 1水平下降。仅在一项任务中包含庞大的单体计算工作就足以让 UI 畅通无阻。当然,如果您有很多处理器/内核,仍然值得并行化内部可能发生的任何事情,但与 I/O 相比 - 拆分太多,您将忙于切换任务而不是咀嚼计算。

    总结:如果您有耗时的 I/O - 异步操作可以节省很多时间。很难过度使用异步 I/O 操作。如果你有占用 CPU 的操作,那么添加任何东西都会消耗更多的 CPU 时间和更多的内存,但是由于将作业分成更小的部分,这些部分可能可以同时在更多内核上运行,因此挂钟时间会更好时间。过度使用并不难,所以你需要小心一点。

    【讨论】:

      【解决方案4】:

      大多数情况下,您不会像可扩展性那样获得直接性能(您正在执行的任务发生得更快和/或内存更少);使用更少的线程来执行相同数量的并发任务意味着您可以同时执行的任务数量更多。

      因此,在大多数情况下,您不会发现给定的操作会提高性能,但会发现大量使用会提高性能。

      如果一个操作需要涉及真正异步(多个异步 I/O)的并行任务,那么这种可扩展性可以使该单个操作受益。因为线程中发生的阻塞程度降低了,即使你只有一个内核也会发生这种情况,因为机器只在那些当前没有等待的任务之间分配时间。

      这与受 CPU 限制的并行操作不同,后者(无论是使用任务还是其他方式)通常只会扩展到可用的内核数量。 (超线程内核在某些方面表现得像 2 个或更多内核,而在其他方面则不然)。

      【讨论】:

        【解决方案5】:

        该方法在当前同步上下文上运行并使用时间 仅当方法处于活动状态时才在线程上。您可以使用 Task.Run 来 将 CPU 绑定的工作移到后台线程,但后台线程 对等待结果的过程没有帮助 可用。

        当您的应用程序中有一个 CPU 和多个线程时,您的 CPU 在线程之间切换以模拟并行处理。使用 async/await 您的异步操作不需要线程时间,因此您可以为应用程序的其他线程提供更多时间来完成工作。例如,您的应用程序(非 UI)仍然可以进行 HTTP 调用,而您所需要的只是等待响应。这是使用 async/await 的好处很大的情况之一。

        当您调用async DoJobAsync() 时,不要忘记.ConfigureAwait(false),以便为不需要合并回 UI 线程上下文的非 UI 应用获得更好的性能。

        我没有提到对保持代码干净有很大帮助的好语法。

        MSDN

        【讨论】:

        • 注意:等待 IO(或其他任何东西)的线程不会被安排执行 - 因此从 CPU 使用率的角度来看,异步任务和阻塞线程之间没有太大区别(除非您的应用正在使用许多线程,如 ASP.Net)。在非 UI 线程(或非 UI 应用程序)上调用 ConfigurAwait(false) 也是没有意义的 - 将没有同步上下文来尝试返回原始线程(显然 ConfigurAwait(false) 对 ASP.Net 完全有害 -但这是常识,因此无需明确指定)。
        【解决方案6】:

        async 和 await 关键字不会导致创建额外的线程。异步方法不需要多线程,因为异步方法不在其自己的线程上运行。该方法在当前同步上下文上运行,并且仅在该方法处于活动状态时才使用线程上的时间。您可以使用 Task.Run 将 CPU 密集型工作转移到后台线程,但后台线程对等待结果可用的进程没有帮助。

        .NET 的 async-await 功能与其他框架没有什么不同。它在本地计算中并没有带来性能优势,但它只是允许在单个线程中的任务之间连续切换,而不是让一个任务阻塞线程。如果您想提高本地计算的性能,请使用任务并行库。

        访问https://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-05-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-10-24
          • 2014-10-20
          相关资源
          最近更新 更多