使用异步等待可以为您带来任何性能优势

本文关键字:任何 性能 异步 等待 | 更新日期: 2024-09-23 00:44:14

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

有没有任何例子可以说明如何使用async-await来弥补算法的性能优势?就像让我们来回答任何一个经典的编程面试问题:

  • 在二叉树中查找最近的共同祖先
  • 给定a[0]a[1]。。。,a[n-1]表示以10为基数的数字,找到使用相同数字的下一个最高数字
  • 找到两个排序数组的中值(即,如果要合并它们,则为中值)
  • 给定数字12、…的数组。。。,n缺少一个号码,查找缺少的号码
  • 查找数组中最大的2个数字

有什么方法可以让那些使用async-await的人获得性能优势吗?如果是这样的话,如果你只有一个处理器呢?那么,你的机器不是只是在任务之间分配时间,而不是同时完成任务吗?

使用异步等待可以为您带来任何性能优势

在这次采访中,Eric Lippert将异步等待与厨师做早餐进行了比较。它帮助我理解了异步等待的好处。在中间的某个位置搜索"异步等待"

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

方法1:同步。由一个线程执行。你开始烤面包。等面包烤好。取出面包。开始烧水,等到水开了再把鸡蛋放进去。等到鸡蛋做好了再把鸡蛋拿出来。开始煮开水泡茶。等到水开了再泡茶。

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

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

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

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

补充:在其他用户的一些评论和测试之后,我发现事实上,任何线程都可以在等待之后继续你的工作。另一个线程具有相同的"上下文",因此可以将其视为原始线程。

方法3:雇佣厨师在泡茶时烤面包和煮鸡蛋:真正的异步。多个线程这是最昂贵的选项,因为它涉及创建单独的线程。在做早餐的例子中,这可能不会大大加快这个过程,因为在这个过程中,你大部分时间都没有做任何事情。但是,例如,如果你还需要切西红柿,那么让一个厨师(单独的线程)来做这件事可能会很方便,同时你可以使用异步等待来做其他事情。当然,你要做的一件事就是等待厨师完成切片。

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

每当我读到关于async await的文章时,用例示例总是有一个你不想冻结的UI。

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

有没有任何例子可以说明如何使用异步等待来在算法中获得性能优势?

没有。

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

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

我的博客上有一篇关于async的介绍,还有一篇关于如何使async不使用线程的文章。

请注意,任务并行库使用的任务可以调度到线程上,并将执行代码。基于任务的异步模式使用的任务没有代码,也不"执行"。尽管这两种类型的任务由相同的类型(Task)表示,但它们的创建和使用完全不同;我在博客中更详细地描述了这些委派任务和承诺任务。

简而言之,非常普遍的情况是-不,通常不会。但它只需要几句话,因为"性能"可以用很多方式理解。

只有当"作业"是I/O绑定的时,异步/等待才能"节省时间"。任何将它应用于CPU绑定的作业都会带来一些性能打击。这是因为,如果你的CPU上有一些计算需要10秒,那么添加async/await(即:任务创建、调度和同步)只会在10秒的基础上增加X额外的时间,而你仍然需要在CPU上消耗这些时间来完成任务。接近阿姆达尔定律的概念。不是真的,但很接近。

然而,也有一些"但是…"s.

首先,通常由于引入async/await而导致的性能打击并没有那么大。(尤其是如果你小心不要做过头的话)。

其次,由于async/await允许您更容易地编写I/O交错代码,您可能会注意到在您太懒(:)而无法执行其他操作的地方,或者在没有async/await语法优势的情况下,它会使代码难以执行的地方,有新的机会来消除I/O上的等待时间。例如,围绕网络请求拆分代码是一件很明显的事情,但您可能会注意到,也就是说,您还可以在写入CSV文件或读取配置文件等的几个地方升级一些文件i/o。不过,请注意,这里的收益并不是由于async/await,而是由于重写了处理文件i/o的代码。您也可以在没有async/await的情况下完成此操作。

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

第四:UI。你真的不想冻结它。把I/O和CPU绑定的作业都包装在Tasks中,并对它们进行异步/等待,以保持UI的响应非常容易。这就是为什么你会看到到处都提到它。然而,理想情况下,I/O绑定的操作应该是异步的,以消除所有冗长I/O上的空闲等待时间,而CPU绑定的作业不需要拆分或异步化,只需要降低一级。将庞大的单片计算工作封装在一个任务中就足以解除UI的阻塞。当然,如果你有很多处理器/内核,那么仍然值得对内部可能的任何东西进行并行化,但与I/O相比,拆分太多,你将忙于切换任务,而不是咀嚼计算。

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

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

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

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

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

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

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

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

我没有提到有助于保持代码整洁的好语法。

MSDN

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

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

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

async使您的应用程序响应更快,但它确实带来了较小的性能开销。

一个真实世界的例子:

我们刚刚将巨大的大量使用的ASP.NET Core应用程序重写为完全CCD_;一直往下";(在有意义的地方——I/O、http上的外部API请求、SMTP/IMAP等电子邮件协议),遵循Stephen Cleary的博客/书籍中的所有最佳实践,结果是:

优点:

  1. 该应用程序的响应速度更快,屏蔽更少
  2. 该应用程序使用较少的线程(从而保护自己不会使线程池和防止"线程池";线程饥饿";在非常重的负载下)

缺点:

  1. 进程的平均CPU负载增加了3-4%(从5-6%增加到8-10%)

在分析应用程序后,大部分CPU周期(约15%)都用于调度任务、继续操作和保存/恢复状态。内部低级别的TPL,如RunOrScheduleActionIAsyncStateMachineBox

YMMV。测量所有

编辑:有关如何最大限度地减少这种开销的提示,请参阅Stephen Toub的视频https://www.youtube.com/watch?v=zjLWWz2YnyQ