【技术分享】线程池核心线程数与最大线程数的区别|任务数超过最大线程数怎么办

嘿,你问我关于线程池的事......我自己也经历过这些漏洞,我会告诉你我发现了什么。

上周一位客户问我为什么他的在线系统突然卡住了。
经过检查,发现线程池爆了。
当时我就想,这家伙肯定是把最大线程数设得太低了。
核心线程数和最大线程数这个问题,确实应该慎重考虑。

核心线程数(corepulsesize),说白了就是线程池中必须保持活动状态的最小线程数。
你想想,如果核心线程太少,工作一来就得等,系统肯定卡住了。
比如2 02 3 年,我在上海的一个购物中心举办了一个活动,表演了1 0首背景序列作品。
我测试的时候把核心线程数设置为8 ,你猜结果是什么?任务一输入就必须排队,稍后处理用户命令会变得更慢。
后来我把核心线程数增加到1 5 个,现在好多了。

最大线程数(maximumPoolSize)是线程池可以支持的最大线程数。
请注意,核心线程数不能超过最大线程数。
这是常识。
想一想,如果主数是5 ,最大数是3 ,你怎么能这样做呢?那太远了。

当你的任务到达时,线程池首先检查是否有足够的当前线程。
如果还不够,未达到课程数量,则创建一个新线程并让它完成工作。
直到核心线程数满了,然后继续查看工作队列(workQueue)。

如果队列未满,作业将排队等待。
如果队列已满,请确保达到最大线程数。
如果没有,则创建一个新线程并运行该任务。
到了吗?这很有趣,纱线池要按照你准备好的图案来做。

最令人惊讶的是堕胎政策,直接造成了例外。
任何人都知道如何使用它。
CallerRunsPolicy 更加激进。
它直接让提交任务的线程自行运行。
也许整个性别线索将会完整。
DiscardPolicy 非常粗鲁。
它只是在作业到达时将其丢弃,这对用户来说是不幸的。
DiscardOldestPolicy 稍微好一点,队列中的第一个任务,为新任务腾出空间通过抛出
之前我在做一个使用DiscardOldestPolicy的项目,但我发现有时系统执行任务非常慢。
当我查看时,队列中已经排满了几个月前的工作。
后来我改成了CallerRunsPolicy,用户提交的线程会自己运行。
虽然CPU占用率高,但至少工作没有丢失,用户看着也舒服。

不管怎样,设置这两个数字应该根据你的业务特点而定。
特别是作业较多且处理时间不长的情况下,可以增加最大线程数。
如果任务处理时间较长,可以适当减少,这样就不会浪费太多的线程和资源。

这个问题没有标准答案。
你必须亲自尝试一下。
如果不确定,可以先保守设置,慢慢调整。
调整后,系统性能应稳定。

一个进程最多可以创建多少个线程?

3 2 位系统,8 MB堆栈空间,最多3 8 0个线程; 6 4 位系统,理论1 6 00万,实际系统参数。
系统参数如threads-max默认为1 4 5 5 3 ,pid_max默认为3 2 7 6 8 ,max_map_count默认为6 5 5 3 0。
3 2 位虚拟内存,6 4 位系统参数。
检查参数、更改参数、优化线程池模型。

.NET 解决new Thread().Start 导致高并发CPU 100%的问题

说白了,直接使用new Thread().Start()在高并发场景下会导致CPU尖峰。
核心问题是线程创建和删除过于频繁,上下文切换代价高昂。
这个问题因资源管理的细节而变得复杂。
我们先来说说最重要的事情。
每个线程创建需要 1 MB 堆栈内存。
去年我们跑一个电商闪购项目,直接开启3 000个并发线程,结果5 分钟系统就崩溃了。
这种现象的技术术语是雪崩效应。
事实上,前面的一个小延误就导致了后面的一切。
还有一点是,当线程数超过CPU核心数的2 倍时,切换成本占比超过2 0%。
我们测试了运行8 0个线程的四核系统,实际有效计算时间只有正常时间的一半。
还有另一个重要的细节。
线程栈是GC的根。
去年1 2 月份,网上发生了线程池内存溢出的情况。
原因是任务执行时间过长,堆栈没有回收。
当时以为是简单的CPU问题,后来发现不对劲,检查GC日志才发现。

一开始我以为线程池就像调整SetMaxThreads一样简单,但是我发现我必须相应地调整系统负载和任务类型。
例如,我们必须颠倒 I/O 密集型任务和 CPU 密集型任务的线程比率。
等等,还有一件事。
我可以分离消息队列,但去年我在连接Kafka时忘记配置消息重试,导致大量工作丢失。
很多人没有注意到这一点。

我们建议您首先尝试 TPL。
async/await对于前端开发更加友好。
如果您不确定,请尝试使用消息队列。

【技术分享】线程池核心线程数与最大线程数的区别|任务数超过最大线程数怎么办

说实话,之前我对纱线池了解不多,直到偶然发现它我才意识到。
主线程数和最大线程数的概念说起来很容易,但是如果能在一个场景中解释清楚,那就真的很混乱了。

核心线程数(corePoolSize),说白了就是线程池中“上岗”的普通工人。
比如你创建一个线程池,设置主线程数为5 ,那么这5 个线程就会常驻。
你提交一个新任务,只要这5 个人闲着,就让他们接。
完成任务后,这五个线程不会消失,等待下一个任务。
为什么要这样设置呢?想想看,如果每次有任务到来就创建一个新线程,使用时就删除它,那么频繁的创建和销毁会消耗大量的性能。
比如我们上线电商闪购系统的时候,高峰期一到就创建了几百上千个线程,CPU直接烧掉。
最后,我们改变了主线程的数量并添加了适当的队列,性能立即得到改善。

有趣的是最大线程数(maximumPoolSize)。
这就像公司规则一样。
即使你很忙,最多也不能超过这个人数。
之前面试的时候面试官就问过我这个问题。
他说你的系统并发量已经测试过3 000QPS。
您放入的最大线程数是多少?当时我就一头雾水,说设置为1 00。
面试官说这肯定是错的。
3 000QPS 应该平均每秒处理 3 0 个请求。
如果只开启1 00个线程,队列会如何爆炸?后来我意识到最大线程数取决于队列的大小和任务的执行时间。
比如设置1 00个线程,队列长度为1 000,那么可以处理的并发量远大于1 00。

至于任务数超过最大线程数时的处理策略,这简直就是线程池的精神。
我记得曾经在那个报告系统中工作过。
任务太长了。
主线程1 0个,最多5 0个线程,队列5 00个,结果半夜监控报警,任务堆积。
经过大量研究,我发现第三方API调用非常慢,导致队列被填满。
这时候就看你如何设置你的饱和策略了。

AbortPolicy 是最直接的事情。
任务完成时抛出异常。
当我们为金融系统工作时,我们不敢使用它。
如果一项重要任务被拒绝,将直接导致交易异常,造成严重后果。
CallerRunsPolicy 将任务扔给调用者的线程。
我试了一次,服务器CPU达到了9 0%。
用户认为系统被阻止。
DiscardPolicy是最狠的。
当任务已满时,它将拒绝任务。
我们将它用于非关键日志处理。
毕竟,几万条日志是可以删除的。
最常用的是 DiscardOldestPolicy,它首先丢弃最旧的任务集,为新任务腾出空间。
我们经常使用它,但我们必须小心不要错过关键任务。

自定义策略更加灵活,但实施起来比较复杂。
我们曾经想制定折扣策略。
当任务完成后,我们我们首先降低 API 并返回它。
后来我们发现,比直接使用现成的策略更容易出现错误。
所以,如果没有特殊需求,一般还是坚持内置的吧。

归根结底,基本的线程管理就是日常,线程管理的最大限制,饱和策略管理爆了之后怎么办。
这个问题没有标准答案,要看你的业务场景。
例如,当我们做秒杀系统,使用ThreadPoolExecutor时,最后的选择是CallerRunsPolicy,因为任务到达必须立即处理,不能等待,但不能直接抛出异常让用户等待。
我个人没有以这种方式运行Java 8 +的ThreadPoolExecutor.newWorkStealingPool,但我听说事物的队列是无限的,这可能是另一个问题。
我记得数据是1 .5 倍的CPU核心数适合多任务,但实际情况要看你任务的特点。