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

Vil*_*lx- 15 performance multithreading asynchronous

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

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

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

那么......我错过了房间里的哪头大象?

Ste*_*ary 12

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

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

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

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

不.使用的I/O任务async不要在任何地方"运行",也不需要"切换"到.在Windows上,I/O任务使用下面的IOCP,I/O任务不"运行" - 它们只是"完成",这是系统中断的结果.更多信息在我的博客文章"没有线程".

效率从何而来?

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

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


Dav*_*aim 5

(在这个答案中,我将讨论 .NET,因为它是第一个随 .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. 难怪使用Tasks 进行编程变得如此流行。

Task.ContinueWith是高度不可读和不可写的。
基本上,将一个任务链接到一个任务到一个任务到一个任务......是一个令人头痛的问题。正如 Node.js 开发人员抱怨的那样(即使在 JS 中async/await也会在未来的某个时候标准化)。async/await 可以解决这个问题。基本上,C# 编译器在幕后做了一个巧妙的巫术。简而言之,它接受后面的所有内容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 是最初await在 C++ 中提供标准化的 (Microsoft) 开发人员。在他 2015 年的演讲中,他表明 C++ 版本await实际上比回调式异步网络 IO 的性能更高。(切换到39:30)

对于具体问题:

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

错误的async/await编译为状态机。任务继续完成后会调用该状态机。无论如何,任务在线程池上运行。async/await产生与多线程代码相同的调度量 / 对线程池工作进行排队。重要的是您获得的简单性。

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

错误的。再次,async/await编译为状态机。当任务完成时调用时,它将为本地变​​量使用相同数量的堆栈内存。无论如何,延续都会在线程(通常是线程池线程)上运行,因此该参数无效。

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

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

正是这种简单性使得async/await如此吸引人。编写实际上是异步的同步代码。如果这不是“高级编程”,那什么才是?