ChatGPT解决这个技术问题 Extra ChatGPT

多少个线程太多了?

我正在编写一个服务器,并在收到请求时将每个操作发送到一个单独的线程中。我这样做是因为几乎每个请求都会进行数据库查询。我正在使用线程池库来减少线程的构造/销毁。

我的问题是:像这样的 I/O 线程的好的截止点是什么?我知道这只是一个粗略的估计,但我们是在谈论数百个吗?数千?

我将如何弄清楚这个截止点是什么?

编辑:

谢谢大家的回复,看来我只需要对其进行测试以找出我的线程数上限。但问题是:我怎么知道我已经达到了那个天花板?我到底应该测量什么?

@ryeguy:这里的重点是,如果一开始没有性能问题,您不应该在线程池中设置任何最大值。大多数将线程池限制为约 100 个线程的建议都是荒谬的,大多数线程池的线程数都比这多,而且从来没有问题。
ryeguy,除了我的回答之外,请参阅下面关于测量的内容。
不要忘记 Python 是天生的,并不是真正的多线程友好。在任何时间点,都会执行单个字节码操作码。这是因为 Python 使用了全局解释器锁。
@Jay D:我想说你达到天花板的那一刻就是你的表现开始下降的时候。
@GEOCHET“这里的重点是你不应该在线程池中设置任何最大值”嗯......说什么?固定大小的线程池具有优雅降级和可伸缩性的好处。例如,在网络设置中,如果您基于客户端连接生成新线程,而没有固定的池大小,您将面临非常危险的学习(艰难的方式)您的服务器可以处理多少线程以及每个连接的客户端会受苦。一个固定大小的池就像一个管道阀门,它不允许你的服务器试图咬掉它可以咀嚼的东西。

p
paxdiablo

有人会说两个线程太多了-我不太喜欢那个阵营:-)

这是我的建议:衡量,不要猜测。一个建议是使其可配置并最初将其设置为 100,然后将您的软件发布到野外并监控会发生什么。

如果您的线程使用量在 3 处达到峰值,那么 100 太多了。如果它在一天中的大部分时间都保持在 100,请将其提高到 200,看看会发生什么。

实际上,您可以让您的代码本身监控使用情况并在下次启动时调整配置,但这可能有点矫枉过正。

为了澄清和阐述:

我不提倡滚动你自己的线程池子系统,一定要使用你拥有的那个。但是,由于您询问线程的良好截止点,我假设您的线程池实现能够限制创建的最大线程数(这是一件好事)。

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

最小活动线程数。

最大线程数。

关闭一段时间未使用的线程。

第一个为线程池客户端的最低性能设置了基线(此线程数始终可供使用)。第二个对活动线程的资源使用设置了限制。第三个让您在安静时间返回基线,以最大限度地减少资源使用。

您需要平衡未使用线程的资源使用 (A) 与没有足够线程来完成工作的资源使用 (B)。

(A) 通常是内存使用情况(堆栈等),因为不做任何工作的线程不会使用太多 CPU。 (B) 通常会在请求到达时延迟处理请求,因为您需要等待线程可用。

这就是你测量的原因。正如您所说,您的绝大多数线程将等待来自数据库的响应,因此它们将不会运行。有两个因素会影响您应该允许的线程数。

第一个是可用的数据库连接数。这可能是一个硬限制,除非您可以在 DBMS 上增加它 - 我将假设您的 DBMS 在这种情况下可以接受无限数量的连接(尽管理想情况下您也应该测量它)。

然后,您应该拥有的线程数取决于您的历史使用情况。您应该运行的最小值是您曾经运行过的最小值 + A%,绝对最小值为(例如,使其像 A 一样可配置)5。

最大线程数应该是你的历史最大值 + B%。

您还应该监控行为变化。如果由于某种原因,您的使用量在很长一段时间内达到 100% 可用(这样会影响客户端的性能),您应该提高允许的最大值,直到它再次高出 B%。

回应“我到底应该测量什么?”问题:

您应该具体测量的是负载下并发使用的最大线程数(例如,等待从 DB 调用返回)。然后添加例如 10% 的安全系数(强调,因为其他海报似乎将我的示例作为固定建议)。

此外,这应该在生产环境中进行调整。事先得到一个估计是可以的,但你永远不知道生产会怎样(这就是为什么所有这些东西都应该在运行时可配置的原因)。这是为了捕捉诸如意外加倍的客户端调用的情况。


如果线程是在传入请求中产生的,那么线程使用将反映未服务请求的数量。无法从中确定“最佳”数字。实际上,您会发现更多的线程会导致更多的资源争用,因此活动线程的数量会增加。
@Andrew,线程创建需要时间,您可以根据历史数据 [+ N%] 确定最佳数量(因此衡量,不要猜测)。此外,更多线程只会在它们工作时引起资源争用,而不是等待信号/信号量。
使用线程池时导致性能问题的“线程创建”数据在哪里?一个好的线程池不会在任务之间创建和销毁线程。
@Pax如果您的所有线程都在等待相同的信号量来运行数据库查询,那么这就是争用的定义。说线程在等待信号量时不花费任何成本也是不正确的。
@Andrew,我不明白您为什么要信号量阻止数据库查询,任何体面的数据库都将允许并发访问,并且有许多线程在等待响应。并且线程在信号量阻塞时不应该花费任何执行时间,它们应该坐在阻塞队列中,直到信号量被释放。
J
Jay D

这个问题已经讨论得很透彻了,我没有机会阅读所有的回复。但是,在查看可以在给定系统中和平共存的同时线程数的上限时,需要考虑以下几点。

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

现在您可以调整堆栈大小以包含更多线程,但是您必须考虑线程管理的开销(创建/销毁和调度)。您可以对给定进程和给定线程强制执行 CPU Affinity,以将它们绑定到特定 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/


请注意,这些虚拟内存限制仅适用于 32 位系统。在 64 位上,您不会耗尽虚拟内存。
@JanKanis,这是一个很好的观点,我记得当第一个 64 位大型机到达时看到了一些分析,有人计算出将整个地址空间交换到磁盘需要一两个月(不记得确切的时间,但它是一样的荒谬的)。
@paxdiablo 很想读到。任何指向白皮书等的链接?谢谢
A
Andrew Grant

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

“最好的情况”是,您后面的线程将在第一个线程完成时停止,或者某些线程在资源争用较低的情况下具有低开销块。最坏的情况是您开始破坏缓存/磁盘/网络,并且您的整体吞吐量下降到最低点。

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

然后,可以根据分析结果、正在运行的硬件以及机器上可能发生的其他情况,调整和缩放此池中的活动线程数。


是的,它应该与队列或请求池结合使用。
@安德鲁:为什么?每次收到请求时,它都应该向线程池添加一个任务。当有可用线程时,由线程池为任务分配线程。
那么当你有数百个请求进入并且没有线程时你会怎么做?创造更多?堵塞?返回错误?将您的请求放在一个可以根据需要尽可能大的池中,然后在线程空闲时将这些排队的请求提供给您的线程池。
“创建许多线程来执行许多任务,这些任务通常组织在一个队列中。通常,任务比线程多得多。一旦线程完成其任务,它将从队列中请求下一个任务直到所有任务都完成。”
@Andrew:我不确定 OP 使用的是什么 python 线程池,但是如果您想要我描述的此功能的真实示例:msdn.microsoft.com/en-us/library/…
C
Chad Okere

您应该记住的一件事是,python(至少是基于 C 的版本)使用所谓的 global interpreter lock,它会对多核机器的性能产生巨大影响。

如果您真的需要最大限度地利用多线程 python,您可能需要考虑使用 Jython 或其他东西。


阅读本文后,我尝试在三个线程上运行 Eratosthenes 筛分任务。果然,它实际上比在单个线程中运行相同的任务慢 50%。感谢您的提醒。我在分配了两个 CPU 的虚拟机上运行 Eclipse Pydev。接下来,我将尝试一个涉及一些数据库调用的场景。
有两种(至少)类型的任务:CPU 密集型(例如图像处理)和 I/O 密集型(例如从网络下载)。显然,GIL“问题”不会过多影响 I/O 绑定任务。如果您的任务受 CPU 限制,那么您应该考虑多处理而不是多线程。
是的,如果你有很多网络 io,python 线程会有所改进。我将其更改为线程,并且比普通代码快 10*...
b
bortzmeyer

正如 Pax 所说,衡量,不要猜测。我为 DNSwitness 所做的事情和结果令人惊讶:理想的线程数比我想象的要高得多,大约需要 15,000 个线程才能获得最快的结果。

当然,这取决于很多事情,这就是为什么你必须衡量自己。

完成 Combien de fils d'exécution ? 中的小节(仅限法语)。


15,000?这也比我预期的要高一点。不过,如果这就是你得到的,那么这就是你得到的,我无法反驳。
对于这个特定的应用程序,大多数线程只是在等待来自 DNS 服务器的响应。因此,在挂钟时间中,并行性越多越好。
我认为,如果您有 15000 个线程在某些外部 I/O 上阻塞,那么更好的解决方案将是大量减少线程但使用异步模型。我从这里的经验说。
M
Matthew Lund

我编写了许多大量多线程的应用程序。我通常允许由配置文件指定潜在线程的数量。当我针对特定客户进行调整时,我将这个数字设置得足够高,以至于我对所有 CPU 内核的利用率都非常高,但并没有高到我遇到内存问题(这些是 32 位操作系统)时间)。

换句话说,一旦你遇到瓶颈,比如 CPU、数据库吞吐量、磁盘吞吐量等,添加更多线程不会提高整体性能。但在您达到这一点之前,请添加更多线程!

请注意,这是假设相关系统专用于您的应用程序,并且您不必很好地玩(避免挨饿)其他应用程序。


你能提到一些你见过的线程数吗?了解它会有所帮助。谢谢。
H
Hot Licks

“大铁”的答案通常是每个有限资源一个线程——处理器(CPU 绑定)、arm(I/O 绑定)等——但只有当你可以将工作路由到正确的线程以获取资源时,它才有效被访问。

在不可能的情况下,请考虑您拥有可替代资源 (CPU) 和不可替代资源 (arm)。对于 CPU,将每个线程分配给特定的 CPU 并不重要(尽管它有助于缓存管理),但对于 arm,如果您不能为 arm 分配线程,您将进入排队理论以及保持 arm 的最佳数量是多少忙碌的。一般来说,我认为如果你不能根据使用的手臂路由请求,那么每个手臂有 2-3 个线程将是正确的。

当传递给线程的工作单元没有执行合理的原子工作单元时,就会出现复杂情况。例如,您可能让线程在某一时刻访问磁盘,而在另一时刻在网络上等待。这增加了额外线程可以进入并做有用工作的“裂缝”数量,但它也增加了额外线程污染彼此缓存等的机会,并使系统陷入困境。

当然,您必须权衡所有这些与线程的“重量”。不幸的是,大多数系统都有非常重量级的线程(他们所谓的“轻量级线程”通常根本不是线程),所以最好在低端犯错。

我在实践中看到的是,非常细微的差异可以在多少线程是最佳的方面产生巨大的差异。特别是缓存问题和锁冲突会极大地限制实际并发量。


n
newdayrising

要考虑的一件事是将执行代码的机器上有多少个内核。这代表了在任何给定时间可以进行多少线程的硬性限制。但是,如果在您的情况下,预计线程会经常等待数据库执行查询,您可能希望根据数据库可以处理的并发查询数量来调整线程。


不。线程的全部意义在于(在多核和多处理器流行之前)能够模拟在一台只有一个处理器的机器上拥有多个处理器。这就是您获得响应式用户界面的方式——主线程和辅助线程。
@mmr:嗯,不。线程的想法是允许阻塞 I/O 和其他任务。
我所做的声明是,机器上的内核数量代表了在给定时间可以工作的线程数量的硬性限制,这是事实。当然,其他线程可以等待 I/O 操作完成,对于这个问题,这是一个重要的考虑因素。
无论如何 - 你在 Python 中有 GIL,这使得线程仅在理论上是并行的。最多可以同时运行 1 个线程,因此只有响应能力和阻塞操作才重要。
+1 用于实际了解计算机的工作原理。 @mmr:您需要了解似乎有多个处理器和确实有多个处理器之间的区别。 @Rich B:线程池只是处理线程集合的众多方法之一。这是一个很好的,但肯定不是唯一的。
m
mmr

我认为这对您的问题有点回避,但是为什么不将它们分叉到进程中呢?我对网络的理解(从以前的朦胧日子开始,我根本不编写网络代码)是每个传入的连接都可以作为一个单独的进程处理,因为如果有人在你的进程中做了一些讨厌的事情,它不会核对整个程序。


对于 Python 来说尤其如此,因为多个进程可以并行运行,而多个线程却不能。不过成本相当高。您每次都必须启动新的 Python 解释器,并使用每个进程连接到 DB(或使用一些管道重定向,但这也是有代价的)。
在大多数情况下,进程之间的切换比线程之间的切换(整个上下文切换而不是某些寄存器)更昂贵。最后,它在很大程度上取决于您的线程库。由于问题是围绕线程展开的,我认为进程已经没有问题了。
很公平。不过,我不确定这就是为什么我的得分是 -2 的原因,除非人们真的想看到仅线程的答案,而不是包括其他有效的答案。
@mmr:考虑到问题是关于 /thread/ 池的,是的,我认为人们应该期待关于线程的答案。
进程创建可以在启动时完成一次(即,进程池而不是线程池)。在申请期间摊销,这可能很小。他们无法轻松共享信息,但确实为他们购买了在多 CPU 上运行的可能性,因此这个答案很有用。 +1。
h
hyperboreean

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


添加到您的线程数不应随机使您的应用程序崩溃。是有原因的。您最好找出原因,因为在某些情况下,即使线程较少,它也可能会影响您,谁知道呢。
G
GEOCHET

在大多数情况下,您应该允许线程池来处理这个问题。如果您发布一些代码或提供更多详细信息,则可能更容易看出是否有某种原因线程池的默认行为不是最好的。

您可以在此处找到有关其工作原理的更多信息:http://en.wikipedia.org/wiki/Thread_pool_pattern


@Pax:这不是大多数人第一次不想回答手头的问题(或理解它)。我不担心。
m
masfenix

我经常听到与 CPU 内核一样多的线程。


@Rich,至少解释一下原因:-)。此经验法则仅适用于所有线程都受 CPU 限制的情况;他们每人得到一个“CPU”。当许多线程受 I/O 限制时,通常最好拥有比“CPU”更多的线程(引用 CPU 是因为它适用于执行的物理线程,例如核心)。
@Abgan,我不确定,认为 Python 可能会创建“真正的”操作系统线程(在多个 CPU 上运行)。如果你说的是真的(我没有理由怀疑),那么 CPU 数量没有任何影响——线程只有在大多数线程都在等待某些东西(例如 DB I/O)时才有用。
@Rich:当(真正的)线程时,CPU 计数确实有影响,因为您可以真正同时运行多个非等待线程。使用一个 CPU,只有一个 CPU 运行,并且有许多其他线程等待非 CPU 资源带来的好处。
@Pax:我猜你不理解线程池的概念。
@Rich,我理解线程池很好;看来我(和这里的其他人)也比你更了解硬件。使用一个 CPU,即使有其他线程在等待 CPU,也只能运行一个执行线程。两个CPU,两个可以运行。如果所有线程都在等待 CPU,则理想线程数等于...