为什么OS线程被认为是昂贵的?

Chi*_*Lan 27 multithreading threadpool

有许多解决方案适用于实现"用户空间"线程.无论是golang.org goroutines,python的绿色线程,C#的异步,erlang的进程等.这个想法是允许并发编程,即使是单个或有限数量的线程.

我不明白的是,为什么操作系统线程如此昂贵?正如我所看到的,无论哪种方式,你必须保存任务的堆栈(操作系统线程或用户空间线程),这是几十千字节,你需要一个调度程序在两个任务之间移动.

操作系统免费提供这两种功能.OS线程为什么要比"绿色"线程更昂贵?由于每个"任务"都有专用的OS线程,导致性能下降的原因是什么?

usr*_*usr 13

我想修改都铎王朝的答案,这是一个很好的起点.线程有两个主要开销:

  1. 启动和停止它们.涉及创建堆栈和内核对象.涉及内核转换和全局内核锁.
  2. 保持他们的堆栈.

(1)如果你一直在创建和停止它们,这只是一个问题.这通常使用线程池来解决.我认为这个问题实际上已经解决了.在线程池上调度任务通常不涉及到内核的访问,这使得它非常快.开销大约是几个互锁的内存操作和一些分配.

(2)只有当你有很多线程(> 100左右)时,这才变得很重要.在这种情况下,异步IO是一种摆脱线程的方法.我发现如果你没有疯狂数量的线程,同步IO包括阻塞比async IO稍快(你读得正确:同步IO更快).

  • (1)我不确定为什么内核对象比需要锁的用户空间对象贵,而所有锁都归结为OS = kernle锁。我不明白(2)无论如何您都需要保持它们的堆栈。 (2认同)
  • 至于(1):锁不归结为内核锁.对于未退火和/或短期持有的锁,可以进行许多优化.内核对象由于多种原因而具有更多开销(例如,它们可以跨进程共享,可以具有ACL,......).它们还需要内核模式转换. (2认同)
  • 好的,得到1覆盖,谢谢,我想(2)的答案是"一些线程系统以有缺陷的方式实现,所以他们需要一个新的模型来修改他们造成的堆栈问题;-)". (2认同)

jus*_*tin 6

有许多解决方案适用于实现"用户空间"线程.无论是golang.org goroutines,python的绿色线程,C#的异步,erlang的进程等.这个想法是允许并发编程,即使是单个或有限数量的线程.

这是一个抽象层.对于许多人来说,掌握这个概念并在许多情况下更有效地使用它更容易.对于许多机器来说也更容易(假设抽象很好),因为在很多情况下模型从宽度移动到拉伸.使用pthreads(作为示例),您拥有所有控件.对于其他线程模型,我们的想法是重用线程,创建并发任务的过程便宜,并使用完全不同的线程模型.消化这个模型要容易得多; 没有什么可以学习和衡量,结果总体上是好的.

我不明白的是,为什么操作系统线程如此昂贵?正如我所看到的,无论哪种方式,你必须保存任务的堆栈(操作系统线程或用户空间线程),这是几十千字节,你需要一个调度程序在两个任务之间移动.

创建线程很昂贵,堆栈需要内存.同样,如果您的进程使用多个线程,那么上下文切换可能会导致性能下降.因此,轻量级线程模型由于多种原因而变得有用.创建OS线程成为中型到大型任务的理想解决方案,理想情况是数量较少.这是限制性的,维护起来非常耗时.

任务/线程池/用户态线程不需要担心大部分上下文切换或线程创建.它通常是"在资源可用时重用资源,如果它现在还没有准备好 - 同时,确定该机器的活动线程数".

更常见的(IMO),操作系统级别的线程很昂贵,因为工程师没有正确使用它们 - 要么太多而且有大量的上下文切换,对同一组资源存在竞争,任务太小.理解如何正确使用OS线程以及如何将其最佳地应用于程序执行的上下文需要花费更多的时间.

操作系统免费提供这两种功能.

它们可用,但它们不是免费的.它们很复杂,对于良好的性能非常重要.当你创建一个OS线程时,它很快就会给出时间 - 所有进程的时间都在线程之间分配.这不是用户线程的常见情况.当资源不可用时,任务通常会排队.这减少了上下文切换,内存和必须创建的线程总数.当任务退出时,线程被赋予另一个.

考虑时间分布的这种类比:

  • 假设你在赌场.有很多人想要卡片.
  • 您有固定数量的经销商.经销商比想要卡的人少.
  • 在任何给定时间,每个人都没有足够的卡片.
  • 人们需要所有牌来完成他们的比赛/手牌.当他们的比赛/手牌完成时,他们将牌返还给经销商.

您如何要求经销商分发卡片?

在OS调度程序下,这将基于(线程)优先级.每个人每次将获得一张卡(CPU时间),并且将不断评估优先级.

人们代表任务或线程的工作.卡片代表时间和资源.经销商代表线程和资源.

如果有2个经销商和3个人,你会如何处理最快?如果有5个经销商和500个人?你怎么能最大限度地减少用完纸牌?使用线程,添加卡片和添加经销商不是"按需"提供的解决方案.添加CPU相当于添加经销商.添加线程相当于经销商一次向更多人发牌(增加上下文切换).有许多策略可以更快地处理卡片,特别是在您消除了人们在一定时间内对卡片的需求之后.如果经销商与人的比例为1/50,那么在他们的游戏完成之前去一张桌子并与一个人或人交易会不会更快?相比之下,根据优先级访问每个表,并协调所有经销商之间的访问(操作系统方法).这并不意味着操作系统是愚蠢的 - 这意味着创建一个操作系统线程是一个工程师添加更多的人和更多的表,可能比经销商可以合理处理的更多.幸运的是,在许多情况下可以通过使用其他多线程模型和更高的抽象来解除约束.

OS线程为什么要比"绿色"线程更昂贵?由于每个"任务"都有专用的OS线程,导致性能下降的原因是什么?

如果您开发了性能关键的低级别线程库(例如,在pthreads上),您将认识到重用的重要性(并在库中将其作为可供用户使用的模型实现).从这个角度来看,更高级别多线程模型的重要性是基于现实世界使用的简单而明显的解决方案/优化,以及可以降低采用和有效利用多线程的入口条的理想.

并不是说它们很昂贵 - 轻量级线程的模型和池是许多问题的更好解决方案,对于不了解线程的工程师来说更合适的抽象.在此模型下,多线程的复杂性大大简化(在实际使用中通常更具性能).对于OS线程,您确实拥有更多控制权,但必须考虑更多因素以尽可能有效地使用它们 - 注意这些考虑因素可以极大地重新规划程序的执行/实现.通过更高级别的抽象,通过完全改变任务执行流程(宽度与拉动),可以最大限度地减少许多复杂性.


Mar*_*mes 6

保存堆栈是微不足道的,无论其大小如何 - 堆栈指针需要保存在内核中的线程信息块中(因此通常可以保存大多数寄存器,因为它们将被任何软/硬中断推送导致操作系统进入).

一个问题是从用户进入内核需要保护级别的循环周期.这是一个必不可少但令人烦恼的开销.然后驱动程序或系统调用必须执行中断请求的任何操作,然后调度/调度线程到处理器上.如果这导致一个线程从另一个进程抢占一个线程,则还必须交换额外进程上下文的负载.如果操作系统决定运行在另一个处理器核心上的线程而不是处理中断mut的线程被抢占,则会增加更多的开销 - 另一个核心必须是硬件中断的(这是在硬/软中断之上)首先是操作系统.

因此,调度运行可能是一个非常复杂的操作.

通常根据用户代码安排"绿色线程"或"光纤".上下文更改比OS中断等更容易和更便宜,因为在每次上下文更改时都不需要Wagnerian响铃周期,进程上下文不会更改,并且运行绿色线程组的OS线程不会更改.

由于不存在任何东西,绿色线程存在问题.它们由"真正的"OS线程运行.这意味着,如果一个OS线程运行的组中的一个"绿色"线程阻止了OS调用,则该组中的所有绿色线程都将被阻止.这意味着像sleep()这样的简单调用必须由一个状态机"模拟",这个状态机会产生其他绿色线程(是的,就像重新实现操作系统一样).类似地,任何线程间信令.

当然,绿色线程也不能直接响应IO信令,因此在某种程度上有点无法解决任何线程的问题.


Tud*_*dor 5

为每个小任务启动内核线程的问题在于它会产生不可忽略的启动和停止开销,再加上它需要的堆栈大小。

这是第一个重点:线程池的存在是为了让您可以回收线程,以避免浪费时间启动它们以及浪费它们堆栈的内存。

其次,如果您触发线程来执行异步 I/O,它们将花费大部分时间等待 I/O 完成,从而有效地不做任何工作并浪费内存。更好的选择是让单个 worker 处理多个异步调用(通过一些底层调度技术,例如多路复用),从而再次节省内存和时间。

使“绿色”线程比内核线程更快的一件事是它们是用户空间对象,由虚拟机管理。启动它们是一个用户空间调用,而启动一个线程是一个慢得多的内核空间调用。

  • @Tudor,嗯……我明白,但你必须解释原因。操作系统线程需要保持常规线程不需要的东西。两者都必须保留用户线程的堆栈,两者都必须对其进行调度。为什么操作系统线程很贵? (2认同)