有太多的线程会有什么影响?

407
我正在编写一个服务器,当接收到请求时,我会将每个动作都发送到单独的线程中。我这样做是因为几乎每个请求都会进行数据库查询。我使用一个线程池库来减少线程的构造/析构次数。 我的问题是:这种I/O线程的良好截止点是多少?我知道这只是一个大概的估计,但我们是在谈论数百个还是数千个? 我该如何确定这个截止点呢?
编辑: 谢谢大家的回答,看起来我只需要测试一下才能找出我的线程计数上限。问题是,我怎么知道我已经达到了这个上限?我应该测量什么?

1
@ryeguy:这里的重点是,如果一开始没有性能问题,就不应该在线程池中设置任何最大值。大多数限制线程池到 ~100 个线程的建议都是荒谬的,大多数线程池有 /更多/ 的线程,从来没有问题。 - GEOCHET
1
ryeguy,看到我下面回答中关于要测量什么的补充内容。 - paxdiablo
2
不要忘记,Python 本质上并不支持多线程。在任何时刻,只有一个字节码操作码正在执行。这是因为 Python 使用全局解释器锁。 - ASk
1
@Jay D:我认为当你的表现开始下降时,你就已经到达了瓶颈。 - ninjalj
16
这里的重点是你不应该在线程池中设置任何最大值。固定大小的线程池具有优雅降级和可伸缩性的好处。例如,在网络设置中,如果您基于客户端连接生成新线程,如果没有固定的池大小,则会很容易学习到您的服务器可以处理多少个线程(以一种艰难的方式),并且每个连接的客户端都将遭受影响。固定大小的池就像管道阀门一样,通过禁止服务器试图咬下超出其承受范围的东西来起到作用。 - arkon
显示剩余3条评论
13个回答

277

有些人会说使用两个线程太多了——我不完全赞同 :-)

这是我的建议:多测量,少猜测。一个建议是将线程数设置成可配置的,并最初将其设置为100,然后将软件发布到野外并监视发生的情况。

如果您的线程使用峰值为3,则100太多了。如果它在一天的大部分时间都保持在100个线程上,则可以将其增加到200并观察发生了什么。

实际上可以让你的代码自行监控使用情况并调整下一次启动的配置,但这可能过于繁琐。


说明和阐述:

我不建议自己编写线程池子系统,尽管可以使用已有的线程池子。但是,既然您询问线程的好截止点,我假设您的线程池实现具有限制创建的最大线程数量的能力(这是一件好事)。

我编写了线程和数据库连接池代码,它们具有以下功能(我认为对性能至关重要):

  • 最小活跃线程数。
  • 最大线程数。
  • 关闭一段时间未使用的线程。

第一项为线程池客户端设置了最小性能基准线(这些线程始终可用于使用)。第二项对活动线程的资源使用量进行了限制。第三项在静默时间返回您到基线,以最小化资源使用。

您需要平衡具有未使用线程的资源使用(A)与没有足够线程执行工作的资源使用(B)之间的关系。

(A)通常是内存用量(栈等),因为不执行作业的线程不会使用太多的CPU。(B)通常会导致请求处理延迟,因为您需要等待线程变得可用。

这就是为什么你需要进行测量。正如你所说,绝大多数线程都在等待数据库响应时处于空闲状态。有两个因素会影响您应该允许的线程数。

第一个因素是可用的DB连接数。除非您可以在DBMS中增加连接数,否则这可能是一个硬性限制 - 在这种情况下,我将假设您的DBMS可以接受无限数量的连接(尽管您最好也要测量一下)。

其次,您应该根据历史使用情况来确定应该运行的线程数。您应该运行的最小线程数是您曾经启动过的最小线程数加上A%,最低值为5(例如,和A一样可配置)。

线程的最大数量应该是历史最大值加上B%。

您还应该监视行为变化。如果由于某种原因,您的使用率达到了可用量的100%,并持续了相当长的时间(以至于会影响客户端的性能),您应该将允许的最大值提高到再次高出B%。


回复“我应该测量什么?”的问题:

具体应该测量的是负载下并发使用的最大线程数(例如,等待DB调用返回的线程)。然后再增加10%的安全系数(例如)(强调一下,因为其他帖子似乎把我的示例视为固定建议)。

此外,在生产环境中进行优化时需要进行这些测量。预先估计也是可以的,但您永远不知道生产环境会遇到什么问题(这就是为什么所有这些都应该在运行时可配置)。这是为了捕捉诸如客户端调用数量意外翻倍之类的情况。


如果在收到请求时生成线程,则线程使用情况将反映未服务请求的数量。从这个角度来看,没有办法确定“最佳”数量。事实上,您会发现更多的线程会导致更多的资源争用,因此活动线程的数量会增加。 - Andrew Grant
@Andrew,创建线程需要时间,您可以根据历史数据确定最佳数量(因此要测量,而不是猜测)。此外,当线程在等待信号/信号量时,更多线程只会导致资源争用,而不是执行工作。 - paxdiablo
当使用线程池时,“线程创建”数据在哪里引起了性能问题?一个好的线程池不会在任务之间创建和销毁线程。 - GEOCHET
@Pax 如果你的所有线程都在等待相同的信号量来运行数据库查询,那就是争用的定义。而且如果线程在等待信号量时说它们不会产生任何成本也是不正确的。 - Andrew Grant
1
@Andrew,我不明白为什么你要使用信号量阻塞数据库查询,任何好的数据库都可以允许并发访问,并且有很多线程等待响应。而且当被信号量阻塞时,线程不应该消耗任何执行时间,它们应该坐在阻塞队列中,直到信号量被释放。 - paxdiablo
非常好的答案,谢谢!例如,ThreadPoolExecutor支持所有三个提到的配置选项,并且可以在运行时进行更改。它的Javadoc还提到了更多的配置权衡(关于排队、负载削减、线程回收等)。 - Dmitry Timofeev

46

这个问题已经被充分讨论过了,我没有机会阅读所有的回复。但是在考虑可以和平共存于给定系统中的同时线程数量上限时,有几件事情需要考虑。

  1. 线程堆栈大小:在Linux中,默认的线程堆栈大小为8MB(您可以使用ulimit -a来查找)。
  2. 给定操作系统变体支持的最大虚拟内存。 Linux Kernel 2.4支持2GB的内存地址空间。随着Kernel 2.6的出现,它略微增加到3GB。
  3. [1]显示了每个给定Max VM支持的最大线程数的计算方法。对于2.4,结果约为255个线程。对于2.6,这个数字稍微大一些。
  4. 你有什么样的内核调度程序。将Linux 2.4内核调度程序与2.6进行比较,后者提供O(1)调度,不依赖于系统中存在的任务数量,而前者更像是O(n)。因此,内核调度程序的SMP功能也在系统中可持续线程的最大数量方面发挥了很好的作用。

现在,您可以调整堆栈大小以纳入更多线程,但是您必须考虑线程管理(创建/销毁和调度)的开销。您还可以将CPU亲和力强制应用于给定进程以及给定线程,将它们绑定到特定的CPU上,以避免在CPU之间进行线程迁移的开销,并避免冷现金问题。

请注意,一个人可以根据自己的意愿创建成千上万个线程,但当Linux用尽VM时,它只是随机地开始杀死进程(因此线程)。这是为了保持效用曲线不被最大化。(效用函数反映了给定资源量下系统范围内的效用。在这种情况下,CPU周期和内存资源保持不变,随着越来越多的任务,效用曲线逐渐平缓)。
我相信Windows内核调度程序也会采取类似的措施来处理资源过度利用。
[1] http://adywicaksono.wordpress.com/2007/07/10/i-can-not-create-more-than-255-threads-on-linux-what-is-the-solutions/

2
请注意,这些虚拟内存限制仅适用于32位系统。在64位系统上,您不会耗尽虚拟内存。 - JanKanis
1
@JanKanis,这是一个很好的观点,我记得当第一批64位主机到达时,有人计算出将整个地址空间交换到磁盘需要一个月或两个月的时间(我记不清确切的时间,但它同样荒谬)。 - paxdiablo
@paxdiablo会很想阅读那篇文章。有没有白皮书之类的链接?谢谢。 - Jay D
@JayD: 再也没有找到那份白皮书了(但请参考https://www.ibm.com/docs/en/cics-ts/5.4?topic=basics-24-bit-31-bit-64-bit-addressing获取更多信息)。目前最快的固态硬盘是Crucial T700,顺序写入速度约为12G/s。64位是16艾字节,大约相当于160亿吉字节,所以我们将速度取整为16G/s,因为我本质上是懒人 :-)。那将是大约十亿秒,相当于31年多。我认为这些计算是正确的,但如果有错误,请随时纠正我。 - undefined
@JayD:这实际上比我记得的几个月要糟糕得多。而且,无论如何,考虑到当前的T700成本(大约每TB250澳元),那将使你花费大约40亿美元,而且这还不包括用来存放这些硬盘的盒子的成本。虽然我怀疑如果你购买那么多的话可能会得到折扣 :-) - undefined

20

如果你的线程在执行任何类型的资源密集型工作(CPU/磁盘),那么你很少会看到超过一两个线程的好处,太多线程会很快降低性能。

“最佳情况”是你后面的线程将在第一个线程完成时停滞不前,或者某些线程在资源竞争较低的资源上具有低开销块。最糟糕的情况是你开始抢占缓存/磁盘/网络,整体吞吐量急剧下降。

一个好的解决方案是将请求放在池中,然后从线程池中分派到工作线程中(避免持续创建/销毁线程是一个很好的第一步)。

这个池中活动线程的数量可以根据您的性能分析结果、运行硬件和其他可能发生在机器上的事情进行调整和扩展。


是的,它应该与请求队列或池配合使用。 - Andrew Grant
2
@Andrew:为什么?每次收到请求时,它应该向线程池添加一个任务。当有可用线程时,由线程池分配一个线程给该任务。 - GEOCHET
当有数百个请求进来并且没有可用的线程时,您该怎么办?创建更多线程?阻塞?还是返回错误?将您的请求放入一个池中,可以尽可能地大,并在线程池空闲时将这些请求作为线程来处理。 - Andrew Grant
创建了一些线程来执行一些任务,这些任务通常被组织在一个队列中。通常情况下,任务的数量要比线程多得多。一旦一个线程完成了它的任务,它就会从队列中请求下一个任务,直到所有任务都完成为止。 - GEOCHET
@Andrew:我不确定OP使用的是哪种Python线程池,但如果你想要一个真实世界的例子来描述我正在描述的这个功能,请参考http://msdn.microsoft.com/en-us/library/system.threading.threadpool.aspx - GEOCHET

14

有一件事情值得注意的是,Python(至少是基于C的版本)使用了被称为全局解释器锁的东西,在多核机器上可能会对性能产生巨大影响。

如果你真的需要最大程度地利用Python的多线程功能,你可能考虑使用Jython或其他语言。


6
读完后,我尝试在三个线程上运行埃拉托色尼筛选法的任务。结果,它实际上比单线程运行相同的任务慢50%。感谢提醒。我是在被分配了两个CPU的虚拟机上运行Eclipse Pydev的。下一步,我将尝试涉及一些数据库调用的场景。 - Don Kirkby
5
至少有两种任务类型:CPU密集型(例如图像处理)和I/O密集型(例如从网络下载)。显然,GIL“问题”不会太影响I/O密集型任务。如果您的任务是CPU密集型,则应考虑使用多进程而不是多线程。 - iutinvg
1
是的,Python线程在处理大量网络IO时有所改进。我将代码改为使用线程后,速度比普通代码快了10倍... - user4985526

9
正如Pax所说,“衡量,而非猜测”。这就是我为 DNSwitness所做的事情,结果令人惊讶:最理想的线程数量比我想象的要高得多,大约需要15,000个线程才能获得最快的结果。
当然,这取决于很多因素,这就是为什么你必须自己进行测量。
完整的测量数据(仅限法语)在 Combien de fils d'exécution ?中。

2
15,000?比我预期的略高了一点。不过,如果这就是你得到的数字,那就是你得到的,我也没有什么好争论的。 - paxdiablo
2
对于这个特定的应用程序,大多数线程只是在等待来自DNS服务器的响应。因此,在墙钟时间方面,并行性越高越好。 - bortzmeyer
24
我认为,如果您有15000个线程正在等待某些外部I/O,则更好的解决方案是使用异步模式,而不是大量减少线程数量。我在这方面有经验。 - Steve
@Steve 我有一个异步系统,但如果使用的线程太少,由于内部实现(网络、nio2等)可能更容易挂起。 - Mladen Adamovic
@MladenAdamovic 我同意线程太少可能会成为问题,但现在的问题是有多少线程才算太多。最小线程数可以通过试错来确定以获得最佳结果。 - Steve

6
我写过许多高度多线程的应用程序。通常,我允许通过配置文件来指定潜在线程数量。当我为特定客户进行调整时,我会将数量设置得足够高,以使我对所有CPU核心的利用率非常高,但不会导致内存问题(这些当时是32位操作系统)。
换句话说,一旦达到某个瓶颈,无论是CPU、数据库吞吐量、磁盘吞吐量等,添加更多线程都不会增加整体性能。但在达到该点之前,请添加更多线程!
请注意,这假设相关系统专门用于您的应用程序,并且您不必与其他应用程序协作(避免饥饿)。

3
你能提到一些你看到过的线数吗?这将有助于了解大致情况。谢谢。 - kovac

5
“大型机器”的处理方式通常是每个有限的资源(处理器(CPU绑定)、arm(I/O绑定)等)分配一个线程,但前提是您可以将工作路由到要访问的资源的正确线程。

如果不可能,考虑一下您拥有可互换资源(CPU)和不可互换资源(arm)。对于CPU来说,将每个线程分配给特定的CPU并不关键(尽管它有助于缓存管理),但对于arm来说,如果您无法将线程分配给arm,那么就会涉及到排队理论以及保持arm繁忙的最佳数量。一般而言,我认为如果无法根据使用的arm路由请求,则每个arm有2-3个线程左右是最合适的。

当传递给线程的工作单元没有执行合理的原子工作单元时,会出现一种复杂性。例如,您可能在一个时间点上使线程访问硬盘,在另一个时间点上等待网络。这增加了“裂缝”的数量,进而使额外的线程可以进入并执行有用的工作,但同时也增加了额外线程污染彼此的缓存等产生负面影响,从而拖慢系统的运行速度。

当然,您必须衡量所有这些,以便考虑线程的“重量”。很不幸,大多数系统都有非常重的线程(他们称之为“轻量级线程”的东西通常根本不是线程),因此最好保持在较低水平。

实践中我发现微小差别可能会对最优线程数产生巨大影响。特别是,缓存问题和锁冲突可以极大地限制实际并发量。”


3

需要考虑的一件事是执行代码的机器上有多少个核心。这代表着同时进行的线程数量的硬性限制。然而,如果像你的情况一样,线程经常会等待数据库执行查询,那么你可能需要根据数据库可以处理的并发查询数量来调整线程。


2
不,线程的整个意义在于(在多核和多处理器变得普遍之前)能够模拟在只有一个处理器的机器上拥有多个处理器。这就是如何获得响应灵敏的用户界面——主线程和辅助线程。 - mmr
1
@mmr:不,线程的概念是为了允许阻塞 I/O 和其他任务。 - GEOCHET
6
我说的是,机器的核心数量代表了同时能够工作的线程数的硬性限制,这是一个事实。当然,其他线程可能正在等待I/O操作完成,对于这个问题来说,这是一个重要的考虑因素。 - newdayrising
2
+1 对于真正理解计算机如何工作的人。@mmr:你需要了解表面上具有多个处理器和实际上具有多个处理器之间的区别。@Rich B:线程池只是处理线程集合的众多方法之一。它是一个好方法,但肯定不是唯一的方法。 - grieve
1
这是错误的。我有一个应用程序,需要大约75个线程才能完全加载8核处理器。为什么?因为存在各种阻塞操作。 - Matthew Lund
显示剩余9条评论

3
我认为这有些回避你的问题,但为什么不将它们分成进程?我的理解是网络编程(从很久以前的模糊记忆,我现在不怎么编写网络代码)中每个传入的连接都可以作为单独的进程处理,因为如果有人在您的进程中做了什么不好的事情,它不会炸掉整个程序。

1
对于Python来说,这一点尤其明显,因为多个进程可以并行运行,而多个线程则不行。然而,代价相当高。每次都必须启动新的Python解释器,并且每个进程都要连接到数据库(或者使用一些管道重定向,但这也是有代价的)。 - Abgan
在大多数情况下,进程之间的切换比线程之间的切换更昂贵(需要进行完整的上下文切换而不仅仅是一些寄存器的切换)。 最后这取决于你使用的线程库。由于问题涉及到线程,我假设进程已经排除在外了。 - Leonidas
好的。我不确定为什么我的得分会被扣2分,除非人们真的想看到仅限于线程的答案,而不包括其他可行的答案。 - mmr
@mmr:考虑到问题是关于/线程/池的,是的,我认为人们应该期望得到关于线程的答案。 - GEOCHET
进程创建可以在启动时进行一次(即进程池而不是线程池)。在应用程序持续时间内分摊,这可能很小。它们不能轻松共享信息,但它确实为它们购买了在多个CPU上运行的可能性,因此这个答案是有用的。+1。 - paxdiablo
@Rich,我已经回答了许多关于原生JS的问题,并在评论中提到“使用jQuery”,即使他们明确排除了jQuery作为选项,意图是改变他们的想法。答案只需要有用,而不必100%相关。 - paxdiablo

2

ryeguy,我正在开发一个类似的应用程序,我的线程数设置为15。不幸的是,如果我将它增加到20,它就会崩溃。因此,我认为处理这个问题的最佳方法是测量您当前的配置是否允许更多或更少的X个线程。


7
增加线程数不应该随意导致应用程序崩溃,这其中一定有原因。你最好找出原因,因为在某些情况下即使线程更少,也可能会受到影响,谁知道呢。 - Matthew Lund

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