异步(非阻塞)代码的可扩展性好处是什么?

13

阻塞线程被认为是一种不良实践,原因有两个:

  1. 线程会占用内存。
  2. 线程通过上下文切换耗费处理时间。

以下是我对这些理由的困惑:

  1. 非阻塞异步代码也应该消耗相同数量的内存,因为在执行异步调用之前,调用堆栈应该保存在某个地方(上下文已经被保存)。而且如果线程显著低效(从内存角度看),为什么操作系统/CLR不提供更轻量级的线程版本(仅保存调用堆栈的上下文)?这难道不是一个更加干净的解决内存问题的方法,而不是强制我们以异步方式重新设计程序(这样做要复杂得多,更难理解和维护)吗?

  2. 当线程被阻塞时,操作系统将其置于等待状态。操作系统不会切换到睡眠线程。由于超过95%的线程生命周期都花费在睡眠中(假设在此处是I/O限制的应用程序),性能影响应该可以忽略不计,因为线程的处理部分可能不会被操作系统抢占,因为它们应该很快运行,做很少的工作。因此,从性能角度来看,我认为非阻塞方法并没有太多好处。

我在这里缺失了什么或者这些论点有什么缺陷吗?


1
并非所有线程都是平等的。阻塞UI线程是不好的,因为它会使应用程序无响应。 - Brian Rasmussen
尝试将您的参数应用于具有响应式用户界面的桌面应用程序和可扩展到许多请求的Web应用程序。 - Wouter de Kort
1
它可以避免你的用户界面变得僵化。这个功能最初是为了让程序员有机会创建WinRT程序,现在被称为UWP而添加到C#中的。诸如打开文件之类的简单操作只能通过异步方法完成。 - Hans Passant
1
@Dark Falcon,这就是我在第一点中问的为什么操作系统/CLR不提供给我们更轻量级的线程版本,而是强制我们将代码迁移到异步架构的原因。如果线程由于某种原因效率低下,请修复这些低效率,但为什么要放弃这个概念呢? - Winston Smith
1
我发现这个帖子很相关:https://dev59.com/3Woy5IYBdhLWcg3wcNrh - Dmitry
显示剩余4条评论
2个回答

17
非阻塞异步代码应该使用相同数量的内存,因为在执行异步调用之前调用堆栈应该会被保存在某个地方(毕竟上下文是保存的)。当await出现时,并不需要保存整个调用堆栈。为什么您认为需要保存整个调用堆栈?调用堆栈是延续的具体表示,并且有待完成任务的延续不是等待的延续。等待的延续留在堆栈上。
现在,当给定调用堆栈中的每个异步方法都等待时,可能会存储与调用堆栈等效的信息在每个任务的延续中。但这些延续的内存负担是垃圾收集堆内存,而不是一百万字节已提交的堆栈内存块。延续状态大小是任务数量的n阶;线程的负担无论是否使用都是一百万字节。
如果线程在内存方面明显低效,为什么操作系统/CLR没有提供更轻量级的线程呢?操作系统确实提供了纤程。当然,纤程仍然有一个堆栈,所以也许并不更好。我想您可以使用一个小堆栈的线程。
假设我们使线程或进程更便宜。这仍然无法解决同步访问共享内存的问题。值得一提的是,如果进程更轻量级,那将是很棒的事情。但他们不是。此外,这个问题有些自相矛盾。你正在使用线程进行工作,因此你已经愿意承担管理异步操作的负担。给定的线程必须能够告诉另一个线程它已经产生了第一个线程所请求的结果。线程已经暗示了异步,但异步并不意味着线程。在语言、运行时和类型系统中构建异步架构只有对那些不得不编写管理线程代码的人有好处。
由于线程的生命周期中超过95%的时间都是休眠(在此假设为IO限制应用程序),因此性能损失应该是可以忽略不计的,因为线程的处理部分可能不会被操作系统抢占,因为它们应该非常快地运行,做很少的工作。
为什么要雇佣一个工人(线程)并支付他们的薪水来坐在邮箱旁边(让线程休眠),等待邮件到达(处理IO消息)?IO中断首先不需要线程。IO中断存在于线程以下的世界。
不要雇佣线程来等待IO; 让操作系统处理异步IO操作。雇佣线程来执行大量高延迟CPU处理,并将一个线程分配给您拥有的每个CPU。
现在我们来看看你的问题:
异步(非阻塞)代码有什么好处?
·不阻塞 UI 线程
·更容易编写在高延迟世界中运行的程序
·更有效地利用有限的CPU资源
但是让我用一个类比来重新表达这个问题。你经营着一个快递公司。有很多订单进来,很多交付出去,而你不能告诉客户说你将不接受他们的交付,直到所有之前的交付都完成。 哪种方式更好:
·雇佣50个人来接电话,取包裹,安排交货和送货,然后要求其中46个人始终处于闲置状态,
还是
  • 雇佣四个人,让每个人一开始就变得非常优秀,一次只做一点工作,以便他们始终能够响应客户的要求,并且在将来很好地保持待办事项清单。

  • 对我而言,后者似乎更划算。


    1
    关于同步 - async/await 模型也无法解决这个问题,因为延续运行在来自线程池的新线程上。 - Winston Smith
    @WinstonSmith:在ASP.NET和控制台应用程序中,续体运行在工作线程上,但在UI线程上等待时,在UI线程上运行。但是,您是正确的,这里确实有许多需要关注的问题。异步体系结构通过在类型系统中表示“将来会出现的值”,而不是让您自己同步访问共享内存,来减轻许多这些困难。 - Eric Lippert
    5
    CLR没有内置的光纤支持。至于你最后说的那点,如果这个工人如此廉价,我为什么要在邮箱旁边支付他的工资呢?当他睡觉时,他不会花费任何金钱(CPU),当他醒来时,他的工作速度非常快,上下文切换问题应该不是问题。当然,还有内存问题,但老实说,无论如何都不是什么大问题。 - Winston Smith
    1
    @WinstonSmith:好的,假设为了论证而言,线程或进程变得非常便宜。这仍然无法缓解“在高延迟操作普遍的世界中有效编写程序”的问题。这就是异步架构旨在解决的问题。无论它是使用少量重型线程还是大量轻型线程来解决问题,都与实际问题无关,即代码是否易于理解。 - Eric Lippert
    1
    我更多地是在谈论可扩展性方面的问题。 - Winston Smith

    -3

    你把多线程和异步概念混淆了。

    你的“困难”都来自于这样一个假设:每个async方法都会被分配一个专用线程来执行工作。然而,事实情况却相反:每当需要执行async操作时,CLR会从线程池中选择一个空闲(已创建)的线程,并在选定的线程上执行该方法。

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


    实际上,异步操作甚至不一定代表线程执行的工作。那只是异步操作的一种类型。许多异步操作根本不涉及使用线程。 - Servy
    @Servy,总有一个线程 - 进程的主线程。许多async操作被安排在该线程上执行而不使用单独的线程,并不意味着没有线程。 - RePierre
    不需要任何线程来执行异步操作。执行CPU绑定操作只是其中一种操作。例如,发送网络请求或延迟一段固定时间时,您甚至不需要主线程,也不需要任何线程。这些操作根本不需要线程。 - Servy
    @Servy,你能否提供一篇描述这些操作的文章?我会编辑我的答案。 - RePierre
    1
    这描述了一些“async”操作根本不需要线程,以及其背后的原理。因为“介绍教程”讨论了“不阻塞当前线程”和“使用当前同步上下文的时间片”,但并没有提到完全没有线程。 - RePierre
    显示剩余2条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接