我理解异步和多线程编程,我已经完成了两个并且可以轻松完成它们。然而有一件事仍然让我感到困惑:为什么普遍认为异步比多线程表现更好? (添加:我正在谈论两种方法都可行并且您可以做出选择的情况)
乍一看原因似乎很清楚 - 线程越少,操作系统调度程序的工作量越少,堆栈空间浪费的内存越少。但是......我不觉得这些论点有用。让我们分别看看它们:
Task
对象而不是精简堆栈框架,基本上每个堆栈框架都包含状态和局部变量以及回调引用和所有内容。此外,它在整个地址空间中都是碎片,这给CPU缓存带来了更多的麻烦,因为预取将是无用的。 那么......我错过了房间里的哪一头大象?
答案 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/EndRead
和BeginWrite/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
而不是多线程代码?
async
/ await
是获取异步结果或异常的最简单方法。 (相信我,我用C ++,C#,Java和Javascript编写异步代码,async
/ await
是该领域的天堂。)async
/ await
适用于CPU绑定任务和IO绑定任务。两个不同但相似的字段的相同界面。 Task
无论如何 一个IThreadPoolItem
并且 被安排到.Net线程池。 async
/ await
只是消除了链接地狱。回到第一步 - >多线程代码。async
/ await
为您同步代码流程。大多数开发人员不是系统开发者他们不知道同步对象和技术的隐藏成本。在大多数情况下,框架提供的实现比普通开发人员可以考虑的平均实现更快。当然,如果你真的尝试,你可以根据自己的需要编写一些非常有针对性的东西,因此性能更高,但这并不适用于大多数开发人员。 await
可能比回调更快。 Gor Nishanov是原始(微软)开发人员,他在C ++中提供了标准await
。 In his 2015 lecture,他表明await
的C ++版本实际上比回调式异步网络IO更高效。 (切换到39:30)具体问题:
操作系统调度程序的工作量减少。真
<强>假即可。 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;,那是什么?