异步(非阻塞)代码的可扩展性优势是啥?

Posted

技术标签:

【中文标题】异步(非阻塞)代码的可扩展性优势是啥?【英文标题】:What are the scalability benefits of async (non-blocking) code?异步(非阻塞)代码的可扩展性优势是什么? 【发布时间】:2016-04-20 08:08:41 【问题描述】:

由于两个主要原因,阻塞线程被认为是一种不好的做法:

    线程消耗内存。 线程通过上下文切换花费处理时间。

以下是我对这些原因的困难:

    非阻塞异步代码也应该消耗几乎相同数量的内存,因为调用堆栈应该在执行异步调用之前保存在某个地方(上下文 保存,之后全部)。如果线程效率非常低(内存方面),为什么 OS/CLR 不提供更轻量级的线程版本(只保存调用堆栈的上下文而不保存其他任何内容)?与其强迫我们以异步方式(明显更复杂、更难理解和维护)重新架构我们的程序,这不是更清洁的内存问题解决方案吗?

    当线程被阻塞时,操作系统会将其置于等待状态。操作系统不会上下文切换到睡眠线程。由于超过 95% 的线程生命周期都花在睡眠上(假设这里是 IO 绑定的应用程序),因此性能损失应该可以忽略不计,因为线程的处理部分可能不会被操作系统抢占,因为它们应该跑得很快,做的工作很少。所以在性能方面,我也看不到非阻塞方法有很多好处。

我在这里遗漏了什么或者为什么这些论点有缺陷?

【问题讨论】:

这里并非所有线程都是平等的。阻塞 UI 线程是不好的,因为它会使应用程序无响应。 如果您想要拥有单线程阻塞代码,那么您当然可以。大多数现代应用程序不喜欢这样做。考虑桌面应用程序中的后台进程,您仍然希望能够与 UI 交互(例如在进程上提供进度表)。或者具有高吞吐量但后端操作繁重的 Web 应用程序,其中占用服务器线程等待操作会大大减少可能的并发用户数量。 它可以避免你的用户界面变得紧张。它被添加到 C# 中首先是为了让程序员有机会创建 WinRT 程序,现在称为 UWP。像打开文件这样简单的事情只能通过异步方法来完成。 @Dark Falcon,这就是为什么我在第一点中问为什么 OS/CLR 不为我们提供更轻量级的线程版本,而不是强迫我们将代码迁移到异步架构?如果线程由于某种原因效率低下,请解决这些低效率问题,但为什么要把这个概念抛到窗外呢? 我发现这个线程是相关的:***.com/q/8546273/625332 【参考方案1】:

非阻塞的异步代码也应该消耗几乎相同数量的内存,因为调用堆栈应该在执行异步调用之前保存在某个地方(毕竟保存了上下文)。

await 发生时保存整个调用堆栈。为什么您认为需要保存整个调用堆栈? 调用栈是延续的具体化等待任务的延续不是等待的延续。 await 的延续在栈上。

现在,很可能的情况是,当给定调用堆栈中的每个异步方法都已等待时,与调用堆栈等效的信息已存储在每个任务的延续中。但是这些延续的内存负担是垃圾收集的堆内存,而不是一百万字节的已提交堆栈内存块。延续状态大小是任务数大小的阶数n;一个线程的负担是一百万字节,不管你是否使用它。

如果线程非常低效(内存方面),为什么 OS/CLR 不提供更轻量级的线程版本

操作系统可以。它提供纤维。当然,纤维仍然有堆叠,所以这可能不是更好。我想你可以有一个带有小堆栈的线程。

这不是内存问题的更清洁的解决方案,而不是强迫我们以异步方式重新架构我们的程序

假设我们使线程——或者就此而言,进程——更便宜。这仍然不能解决同步访问共享内存的问题。

对于它的价值,我认为如果流程更轻量级会很棒。他们不是。

此外,这个问题有些自相矛盾。您正在使用线程,因此您已经愿意承担管理异步操作的负担。当一个给定的线程产生了第一个线程要求的结果时,它必须能够告诉另一个线程。线程已经意味着异步,但异步并不意味着线程。在语言、运行时和类型系统中内置异步架构只会让那些不幸不得不编写管理线程的代码的人受益。

由于超过 95% 的线程生命周期都用于睡眠(假设这里是 IO 绑定的应用程序),因此性能损失应该可以忽略不计,因为线程的处理部分可能不会被操作系统抢占因为它们应该跑得很快,做的工作很少。

你为什么要雇佣一个工人(线程)并支付他们的薪水坐在邮箱(睡觉线程)等待邮件到达(处理 IO 消息)? IO 中断首先不需要线程。 IO 中断存在于线程级别以下的世界中。

不要雇佣线程来等待 IO;让操作系统处理异步 IO 操作。雇用线程来执行极其大量的高延迟 CPU 处理,然后为您拥有的每个 CPU 分配一个线程。

现在我们来回答您的问题:

异步(非阻塞)代码有什么好处?

不阻塞 UI 线程 更轻松地编写生活在高延迟世界中的程序 更有效地利用有限的 CPU 资源

但让我用一个类比来重新表述这个问题。你在经营一家快递公司。有很多订单进来,很多交付出去,你不能告诉客户在他们完成之前的每一次交付之前你不会接受他们的交付。哪个更好:

雇 50 个人接听电话、取包裹、安排送货和运送包裹,然后要求其中 46 人始终处于空闲状态

李>

雇佣四个人,首先让他们每个人非常出色,一次只做一点工作,以便他们始终响应客户的要求,其次,真的善于记下他们未来需要做的工作的待办事项清单

对我来说,后者似乎更划算。

【讨论】:

关于同步 - async/await 模型也不能解决这个问题,因为继续在线程池中的新线程上运行 @WinstonSmith:延续在 ASP.NET 和控制台应用程序的工作线程上运行,但在 UI 线程上等待时它在 UI 线程上运行。但你是对的,这里肯定有需要关注的问题。异步架构试图通过在类型系统中表示“将来会出现的值”来缓解许多这些困难,而不是让您自己同步对共享内存的访问。 而且 CLR 中没有内置对光纤的支持。至于你的最后一点,如果他这么便宜,我为什么要付钱让那个工人坐在邮箱旁边?当他睡觉时,他不花钱(CPU),当他醒着时,他工作得如此之快,以至于上下文切换问题应该不是问题。当然,还有内存问题,但老实说,这不是什么大问题。 @WinstonSmith:好的,假设为了争论,线程——或进程——变得超级便宜。这仍然不能缓解您如何有效地编写生活在高延迟操作普遍的世界中的程序的问题?这就是异步架构旨在解决的问题。是用少量的重线程还是大量的轻线程来解决,与实际问题没有密切关系,即:代码是否可理解? 我更多地谈论的是可扩展性问题。【参考方案2】:

你在这里搞乱了multithreadingasync 的概念。

您的两个“困难”都来自假设每个async 方法都被分配了一个专门的线程来完成工作。然而,情况却完全相反:每次需要执行async 操作时,CLR 从线程池中挑选一个空闲(因此已创建)线程并在所选线程上执行该方法.

这里的核心概念是async 并不意味着总是创建新线程,它意味着在现有线程上调度执行,以便没有线程处于空闲状态。

【讨论】:

实际上,异步操作甚至不一定代表线程完成的工作。这只是 一种 类型的异步操作。许多异步操作根本不使用线程。 @Servy,总有一个线程——进程的主线程。许多async 操作被安排在该线程上执行而不使用单独的线程这一事实并不意味着没有线程。 。异步操作不一定需要 any 线程来完成其工作。执行 CPU 绑定操作只是 一种 类型的操作。例如,在发送网络请求或延迟固定时间段时,您不需要 任何 线程,甚至不需要主线程。这些操作根本不需要线程 @Servy,您能否提供一篇描述此类操作的文章?然后我将编辑我的答案。 描述它们呢?如果您只是想了解有关异步操作如何工作的基本信息,可以阅读异步编程介绍教程。

以上是关于异步(非阻塞)代码的可扩展性优势是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Spring Boot 构建非阻塞异步 Web 服务

PostgreSQL最大的优势是啥?

走近代码之Python--爬虫框架Scrapy

第五章:异步Web服务

读Java性能权威指南(第2版)笔记30_Java服务器

centos7下安装PHP swoole扩展