为什么异步被认为比多线程表现更好?

时间:2017-03-17 12:05:57

标签: multithreading performance asynchronous

我理解异步和多线程编程,我已经完成了两个并且可以轻松完成它们。然而有一件事仍然让我感到困惑:为什么普遍认为异步比多线程表现更好? (添加:我正在谈论两种方法都可行并且您可以做出选择的情况)

乍一看原因似乎很清楚 - 线程越少,操作系统调度程序的工作量越少,堆栈空间浪费的内存越少。但是......我不觉得这些论点有用。让我们分别看看它们:

  1. 操作系统调度程序的工作量减少。没错,但这意味着总共减少工作量吗?仍有N个任务并行运行,SOMEBODY必须在它们之间切换。在我看来,我们只是从OS内核中完成了工作,并开始在我们自己的用户态代码中完成它。但是由于这个原因,需要完成的工作量并没有改变。那么效率来自哪里?
  2. 减少堆栈空间的内存浪费。或者是吗?
    • 首先,我不了解其他操作系统,但至少在Windows中,线程的堆栈空间不能一次全部提交。保留了虚拟内存地址,但实际内存仅在需要时提交。
    • 即使它已经提交,也不重要,因为分配内存并不会减慢你的程序速度。除非你用完它,否则现代计算机有足够的内存容纳成千上万的堆栈,尤其是服务器。
    • 即使堆栈DO被提交并且DO最终导致内存不足,大多数堆栈只会在开始时使用一点(如果你的程序正在调整堆栈溢出,你会遇到更大的问题担心)。这意味着无论如何都可以将大部分页面分页。
    • 大量内存使用的真正问题是CPU缓存会被大量删除。当你在所需的地方获得大量数据时,CPU缓存无法跟上它并且需要一次又一次地从主RAM中获取内容 - 当事情变得缓慢时。但异步编程在任何方面对此都没有帮助。如果有的话,它会积极地使用更多内存。我们现在在堆上分配了单独的Task对象而不是精简堆栈框架,基本上每个堆栈框架都包含状态和局部变量以及回调引用和所有内容。此外,它在整个地址空间中都是碎片,这给CPU缓存带来了更多的麻烦,因为预取将是无用的。
  3. 那么......我错过了房间里的哪一头大象?

2 个答案:

答案 0 :(得分:9)

  

为什么普遍认为异步比多线程表现更好? (补充说:我正在谈论任何一种方法都可行并且你可以做出选择的情况)

在服务器端,async可让您最大限度地利用线程。为什么有一个线程处理单个连接时可以处理数百个?在服务器端,它不是" async vs threads"场景 - 它是一个" async 线程"场景。

在客户端 - 任何一种方法都是真正可行的 - 它并不重要。那么,如果你增加一个额外的不必要的线程呢?即使对于移动应用程序来说,这也不是什么大不了的事。虽然从技术上讲,async可以帮助提高效率,尤其是在内存和电池受限的设备中,在历史的这一点上,它并不是那么重要。但是,即使在客户端,async也有很大的好处,因为它允许您编写串行代码而不是回调回调。

  

仍有N个任务并行运行,SOMEBODY必须在它们之间切换。

没有。 async使用的I / O任务不会运行"在任何地方,不需要"切换"至。在Windows上,I / O任务使用下面的IOCP,而I / O任务不会运行" - 它们只是"完成",这是系统中断的结果。更多信息,请参阅我的博文"There Is No Thread"

  

效率来自哪里?

"效率"很棘手例如,异步HTTP服务器处理程序实际上将比同步处理程序更慢地响应 。使用回调等设置整个异步事件的开销很大。但是,AFAICT的减速是不可测量的,并且异步代码允许该服务器处理比同步服务器更多的同时请求可以(在实际测试中,我们将10倍作为保守估计)。此外,异步代码不受线程池的线程注入速率的限制,因此异步服务器代码会对更快响应加载中的突然更改,从而减少请求超时的数量,与同一场景中的同步服务器相比。同样,这是由于" async 线程"而不是" async而不是线程"。

几年前,Node.js被认为是一个非常高效的服务器 - 基于真实世界的测量。当时,大多数ASP.NET应用程序是同步的(在async之前编写异步应用程序非常困难,而且公司知道只需支付更多服务器硬件就更便宜了)。事实上,Node.js只有运行你的应用程序的一个服务器线程。它是100%异步的,并且它可以从中获得可扩展性。 ASP.NET注意到了这一点,ASP.NET Core(以及其他更改)使整个堆栈异步。

答案 1 :(得分:1)

(在这个答案中,我将讨论.NET,因为它是async/await发布的第一个技术)

我们使用线程来并行化CPU绑定任务,我们使用异步IO来并行化IO绑定任务。

CPU明智:
我们都知道每个任务的线程都是错误的。我们不需要太多线程,因为上下文切换会冻结整个系统。我们不想要太少,因为我们希望尽快完成任务。当然,我们正在寻找某种线程池 ThreadPool是在Task时代之前调度异步任务的默认方式。但线程池有一个痛处 - 很难知道异步何时完成,异步结果或异常是什么。
然后,来了Task。任务不仅在线程池上调度委托,当任务完成时,您可以获得结果或异常,并使用Task.ContinueWith继续使用它们。

IO - 明智:
我们都知道线程连接是一件坏事。如果我们希望我们的优化服务器每秒服务数百万个请求,我们就不能为每个新连接生成一个新线程。我们的系统将在上下文切换时窒息。所以我们使用异步IO。在Task时代之前,我们使用了BeginRead/EndReadBeginWrite/EndWrite,它们容易出错并且只是很痛苦 - 我们不得不使用可怕的事件驱动编程范例
然后,来了Task。我们可以启动异步IO操作,并使用Task.ContinueWith接收结果或异常。它使异步IO更容易使用。

Task是将异步CPU任务与异步IO任务联系起来的粘合剂。使用一个接口,我们可以调度异步函数并使用Task.ContinueWith获得结果。难怪使用Task进行编程变得如此受欢迎。

Task.ContinueWith非常不可读且不可写。
基本上,将任务链接到任务任务到任务......是一件令人头疼的事。正如Node.js开发人员所抱怨的那样(即使在JS async/await中也会在将来的某个时候进行标准化)。 async / await来这里救援。基本上,C#编译器在幕后做了一个整洁的voodo。简而言之,它需要await之后的所有内容,并使用状态机打包它,当await之前的所有内容都完成时调用它。编译器采用同步代码(用async / await注释)并为您执行ContinueWith

那么,为什么要使用async / await + Task而不是多线程代码?

  1. async / await是获取异步结果或异常的最简单方法。 (相信我,我用C ++,C#,Java和Javascript编写异步代码,async / await是该领域的天堂。)
  2. async / await适用于CPU绑定任务和IO绑定任务。两个不同但相似的字段的相同界面。
  3. 如果您想要异步IO,那么线程无论如何都无法帮助您。
  4. Task无论如何 一个IThreadPoolItem并且 被安排到.Net线程池。 async / await只是消除了链接地狱。回到第一步 - >多线程代码。
  5. 任务+ async / await为您同步代码流程。大多数开发人员不是系统开发者他们不知道同步对象和技术的隐藏成本。在大多数情况下,框架提供的实现比普通开发人员可以考虑的平均实现更快。当然,如果你真的尝试,你可以根据自己的需要编写一些非常有针对性的东西,因此性能更高,但这并不适用于大多数开发人员。
  6. 根据您的编程语言,await可能比回调更快。 Gor Nishanov是原始(微软)开发人员,他在C ++中提供了标准awaitIn his 2015 lecture,他表明await的C ++版本实际上比回调式异步网络IO更高效。 (切换到39:30)
  7. 具体问题:

      

    操作系统调度程序的工作量减少。真

    <强>假即可。 async / await编译为状态机。任务继续在完成时调用该状态机。无论如何,在线程池上运行的任务。 async / await产生与多线程代码/排队线程池工作相同的调度量。 它的简单性非常重要。

      

    堆栈空间浪费的内存减少了。或者是吗?

    <强>假即可。再次,async / await编译成状态机。在任务完成时调用时,它将使用相同数量的堆栈内存用于本地变量。无论如何,continuation将在一个线程(通常是一个线程池线程)上运行,因此该参数无效。

      

    为什么异步被认为比多线程更好?

    当您的代码受CPU限制时,Tasks + async / await和纯多线程代码之间会有一点差异。在IO绑定代码中,多线程是您可以拥有的最差吞吐量。任务+ async / await将吹掉任何可以编写自己的IO绑定线程池。线程不会扩展。通常(特别是在服务器端)你有两个。您从连接(IO)读取一些数据,然后继续在CPU上进行处理(json解析,计算等)并将结果写回连接(IO再次)。在这种情况下,任务+ async / await比纯多线程代码更快。

    简单性使async / await如此吸引人。编写实际上异步的同步代码。如果这不是&#34;高级编程&#34;,那是什么?