【问题标题】:Async programming, threads and efficiency异步编程、线程和效率
【发布时间】:2016-12-16 06:17:26
【问题描述】:

我的问题不仅适用于 C# 和 Asp.net,但我更容易提出更具体的问题。

当一个 Asp.net 请求等待异步 IO 操作时,线程基本上会进入线程池以供其他请求重用。为什么这比在 IO 操作完成之前让线程休眠更有效?毕竟,当线程返回到线程池时,它的堆栈需要保存在内存中才能完成最初的请求。我的假设是我们不能重用分配给线程的内存,除非我们将使用过的堆栈内存复制到其他地方,并且复制数据可能会引入额外的开销,这可能是不合理的。

我错过了什么吗?我的假设错了吗?请解释一下。

编辑: jlew 指出的答案缺少一点。当线程返回池中时,请求使用的堆栈内存会发生什么变化?如果我们不能重用内存,那么重用线程有什么意义呢?如果我们想重用堆栈中未使用的部分,那么我们将不得不移动一些内存。如果是这样,移动内存和重用未使用的堆栈内存是否会提高整体效率?

【问题讨论】:

  • 如果您严格查看单个操作,则效率并不高。如果您正在查看一个使用许多线程的应用程序,例如 Web 应用程序,其中需要线程,尤其是在负载较高的情况下,那么它总体上会变得高效,因为现在可以释放服务于 I/O 绑定请求的线程来服务其他线程在 I/O 完成时请求。
  • Thread.Sleep 是一个阻塞调用。除非您正在测试一些冗长的操作,否则不要使用它。 stackoverflow.com/questions/8815895/…
  • 我知道等待 I/O 操作完成的线程可以被释放和重用。我想知道真正的好处是什么。堆栈到哪里去了?是复制还是为后续请求分配了新堆栈?为什么不只是增加线程池中的线程数?除了堆栈内存之外,线程是否会引入任何重大开销?
  • @Vakho 查看来自 jlew 的链接。应该足以回答您的问题。

标签: c# asp.net async-await


【解决方案1】:

为什么这比在 IO 操作完成之前让线程休眠更有效?

将线程视为工人。工人很贵。你想付钱给工人睡觉吗?不。你想付钱给工人做尽可能多的工作;如果他们被阻塞了,你可以让他们做别的事情,而不是在阻塞被清除之前睡觉。

线程非常昂贵。如果线程很便宜,那么可以肯定,我们可以大量生产。您只对昂贵的资源使用池化策略。

线程的开销主要体现在两个方面:您提到堆栈的大小,即每个线程保留 1MB 的虚拟内存。但是在操作系统级别也有很大的成本,它没有针对具有数千个线程的场景进行优化。确定接下来要运行哪个线程,上下文切换到它,并从它切换出去,所有这些都具有随着线程数量增加而增加的非零成本。理想情况下,您希望在 n 处理器机器中有 n 个独立线程,不多也不少。

毕竟,当线程返回到线程池时,它的堆栈需要保存在内存中才能完成原来的请求。

我无法对这句话做出正面或反面。当线程返回池时,它的堆栈指针又回到了底部。

我的假设是我们不能重用分配给线程的内存,除非我们将使用过的堆栈内存复制到其他地方,并且复制数据可能会引入额外的开销,这可能是不合理的。

我开始明白了。你的心智模型是:

  • stack 是 continuation(我们接下来要做什么?)和激活(在这个方法激活中的 locals 的值是什么?)的具体化
  • 已完成的等待任务必须在工作流中的中断点继续,本地任务保持不变
  • 因此,必须将继续/激活信息(堆栈)复制到某处或某处。

这种心智模型是合理的,但却是错误的。异步工作流构建状态机并将延续捆绑为该机器中的状态,然后将对状态机的引用存储在任务中。激活信息从堆栈槽提升到闭包类的字段中。这会将继续/激活信息从堆栈中取出并放入堆中。

现在,请记住,任务的延续并不包含堆栈上的所有信息;从“当前延续的调用”的意义上说,它不是真正的延续,它捕获了当前方法完成时发生的事情。它捕获当前等待的任务完成时发生的情况,这就足够了。

请记住,堆栈只能在“我接下来要做什么?”时用作延续的具体化。工作流在逻辑上是一个堆栈——你接下来要做的事情是堆栈顶部的事情。但是异步工作流形成了一个依赖树,其中等待连接点;它一开始就不是堆栈,因此将延续表示为堆栈是不可能的。

当线程返回池时,请求使用的栈内存会发生什么变化?

只要线程继续存在,为线程堆栈保留的百万字节虚拟内存就会一直保留。认识到这一点很重要!这是 .NET 中线程如此昂贵的原因之一。就操作系统内存管理器而言,这 1MB 的虚拟内存是 100% 使用的,无论该堆栈已被推送到多少。这 1MB 中的大部分在其生命周期的大部分时间里都是垃圾。

当线程返回池时,指向该堆栈的高水位标记的指针被移回开头,并且超出它的所有内容现在都是垃圾。

如果我们不能重用内存,那么重用线程的意义何在?

我们可以并且确实可以重用内存;堆栈指针被重置。

如果是这样,移动内存和重用未使用的堆栈内存是否会提高整体效率?

我没有追随这个问题的主旨;我怀疑这是基于错误的前提。能改一下吗?

【讨论】:

  • 谢谢埃里克。我想我离得够近了,因为你一针见血。我在这个谜题中唯一遗漏的是“异步工作流构建状态机并将延续捆绑为该机器中的状态,然后在任务中存储对状态机的引用。这会从堆栈中获取延续信息并进入堆中。”。非常感谢。无需更多说明。
猜你喜欢
  • 1970-01-01
  • 2016-04-13
  • 2010-09-26
  • 1970-01-01
  • 2011-07-03
  • 2015-03-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多