精选文章 面试官问:高并发下,你都怎么选择最优的线程数?

面试官问:高并发下,你都怎么选择最优的线程数?

作者:Java耿 时间: 2020-08-04 03:19:22
Java耿 2020-08-04 03:19:22

面试官问:高并发下,你都怎么选择最优的线程数?1

为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务。并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行。在高并发的情况下采用线程池,可以有效降低线程创建释放的时间花销及资源开销,如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及 “过度切换”(在 JVM 中采用的处理机制为时间片轮转,减少了线程间的相互切换) 。

但是有一个很大的问题摆在我们面前,即我们希望尽可能多地创建任务,但由于资源所限我们又不能创建过多的线程。那么在高并发的情况下,我们怎么选择最优的线程数量呢?选择原则又是什么呢?

一、理论分析

关于如何计算并发线程数,有两种说法。

第一种,《Java Concurrency in Practice》即《java 并发编程实践》8.2 节 170 页

对于计算密集型的任务,一个有 Ncpu 个处理器的系统通常通过使用一个 Ncpu + 1 个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个 “额外” 的线程,可以确保在这种情况下 CPU 周期不会中断工作)。

对于包含了 I/O 和其他阻塞操作的任务,不是所有的线程都会在所有的时间被调度,因此你需要一个更大的池。为了正确地设置线程池的长度,你必须估算出任务花在等待的时间与用来计算的时间的比率;这个估算值不必十分精确,而且可以通过一些监控工具获得。你还可以选择另一种方法来调节线程池的大小,在一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察 CPU 利用率的水平。

给定下列定义:

Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池的大小等于:
Nthreads = Ncpu x Ucpu x (1 + W/C)

你可以使用 Runtime 来获得 CPU 的数目:

int N_CPUS = Runtime.getRuntime().availableProcessors();

当然,CPU 周期并不是唯一你可以使用线程池管理的资源。其他可以约束资源池大小的资源包括:内存、文件句柄、套接字句柄和数据库连接等。计算这些类型资源池的大小约束非常简单:首先累加出每一个任务需要的这些资源的总量,然后除以可用的总量。所得的结果是池大小的上限。

当任务需要使用池化的资源时,比如数据库连接,那么线程池的长度和资源池的长度会相互影响。如果每一个任务都需要一个数据库连接,那么连接池的大小就限制了线程池的有效大小;类似地,当线程池中的任务是连接池的唯一消费者时,那么线程池的大小反而又会限制了连接池的有效大小。

如上,在《Java Concurrency in Practice》一书中,给出了估算线程池大小的公式:

Nthreads = Ncpu x Ucpu x (1 + W/C),其中
Ncpu = CPU核心数
Ucpu = CPU使用率,0~1
W/C = 等待时间与计算时间的比率

第二种,《Programming Concurrency on the JVM Mastering》即《Java 虚拟机并发编程》2.1 节 12 页

为了解决上述难题,我们希望至少可以创建处理器核心数那么多个线程。这就保证了有尽可能多地处理器核心可以投入到解决问题的工作中去。通过下面的代码,我们可以很容易地获取到系统可用的处理器核心数:

Runtime.getRuntime().availableProcessors();

所以,应用程序的最小线程数应该等于可用的处理器核数。如果所有的任务都是计算密集型的,则创建处理器可用核心数那么多个线程就可以了。在这种情况下,创建更多的线程对程序性能而言反而是不利的。因为当有多个仟务处于就绪状态时,处理器核心需要在线程间频繁进行上下文切换,而这种切换对程序性能损耗较大。但如果任务都是 IO 密集型的,那么我们就需要开更多的线程来提高性能。

当一个任务执行 IO 操作时,其线程将被阻塞,于是处理器可以立即进行上下文切换以便处理其他就绪线程。如果我们只有处理器可用核心数那么多个线程的话,则即使有待执行的任务也无法处理,因为我们已经拿不出更多的线程供处理器调度了。

如果任务有 50% 的时间处于阻塞状态,则程序所需线程数为处理器可用核心数的两倍。如果任务被阻塞的时间少于 50%,即这些任务是计算密集型的,则程序所需线程数将随之减少,但最少也不应低于处理器的核心数。如果任务被阻塞的时间大于执行时间,即该任务是 IO 密集型的,我们就需要创建比处理器核心数大几倍数量的线程。我们可以计算出程序所需线程的总数,总结如下:

  • 线程数 = CPU 可用核心数 /(1 - 阻塞系数),其中阻塞系数的取值在 0 和 1 之间。

  • 计算密集型任务的阻塞系数为 0,而 IO 密集型任务的阻塞系数则接近 1。一个完全阻塞的任务是注定要挂掉的,所以我们无须担心阻塞系数会达到 1。

为了更好地确定程序所需线程数,我们需要知道下面两个关键参数:

  • 处理器可用核心数;

  • 任务的阻塞系数;

第一个参数很容易确定,我们甚至可以用之前的方法在运行时查到这个值。但确定阻塞系数就稍微困难一些。我们可以先试着猜测,抑或采用一些性能分析工具或 java.lang.management API 来确定线程花在系统 IO 操作上的时间与 CPU 密集任务所耗时间的比值。如上,在《Programming Concurrency on the JVM Mastering》一书中,给出了估算线程池大小的公式:

线程数 = Ncpu /(1 - 阻塞系数)

对于说法一,假设 CPU 100% 运转,即撇开 CPU 使用率这个因素,线程数 = Ncpu x (1 + W/C)。

现在假设将方法二的公式等于方法一公式,即 Ncpu /(1 - 阻塞系数)= Ncpu x (1 + W/C),推导出:阻塞系数 = W / (W + C),即阻塞系数 = 阻塞时间 /(阻塞时间 + 计算时间),这个结论在方法二后续中得到印证,如下:

由于对 Web 服务的请求大部分时间都花在等待服务器响应上了,所以阻塞系数会相当高,因此程序需要开的线程数可能是处理器核心数的若干倍。假设阻塞系数是 0.9,即每个任务 90% 的时间处于阻塞状态而只有 10% 的时间在干活,则在双核处理器上我们就需要开 20 个线程(使用第 2.1 节的公式计算)。如果有很多只股票要处理的话,我们可以在 8 核处理器上开到 80 个线程来处理该任务。

由此可见,说法一和说法二其实是一个公式。

二、实际应用

那么实际使用中并发线程数如何设置呢?我们先看一道题目:

假设要求一个系统的 TPS(Transaction Per Second 或者 Task Per Second)至少为 20,然后假设每个 Transaction 由一个线程完成,继续假设平均每个线程处理一个 Transaction 的时间为 4s。那么问题转化为:

如何设计线程池大小,使得可以在 1s 内处理完 20 个 Transaction?

计算过程很简单,每个线程的处理能力为 0.25TPS,那么要达到 20TPS,显然需要 20/0.25=80 个线程。

这个理论上成立的,但是实际情况中,一个系统最快的部分是 CPU,所以决定一个系统吞吐量上限的是 CPU。增强 CPU 处理能力,可以提高系统吞吐量上限。在考虑时需要把 CPU 吞吐量加进去。

分析如下(我们以说法一公式为例):

Nthreads = Ncpu x (1 + W/C)

即线程等待时间所占比例越高,需要越多线程。线程 CPU 时间所占比例越高,需要越少线程。这就可以划分成两种任务类型:

IO 密集型 一般情况下,如果存在 IO,那么肯定 W/C > 1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要在服务器上测试具体多少个线程数适合(CPU 占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取 1 即可,Nthreads = Ncpu x (1 + 1) = 2Ncpu。这样设置一般都 OK。

计算密集型 假设没有等待 W = 0,则 W/C = 0。Nthreads = Ncpu。

根据短板效应,真实的系统吞吐量并不能单纯根据 CPU 来计算。那要提高系统吞吐量,就需要从 “系统短板”(比如网络延迟、IO)着手:

  • 尽量提高短板操作的并行化比率,比如多线程下载技术;

  • 增强短板能力,比如用 NIO 替代 IO;

第一条可以联系到 Amdahl 定律,这条定律定义了串行系统并行化后的加速比计算公式:加速比 = 优化前系统耗时 / 优化后系统耗时 加速比越大,表明系统并行化的优化效果越好。Addahl 定律还给出了系统并行度、CPU 数目和加速比的关系,加速比为 Speedup,系统串行化比率(指串行执行代码所占比率)为 F,CPU 数目为 N:Speedup <= 1 / (F + (1-F)/N)

当 N 足够大时,串行化比率 F 越小,加速比 Speedup 越大。

这时候又抛出是否线程池一定比但线程高效的问题?

答案是否定的,比如 Redis 就是单线程的,但它却非常高效,基本操作都能达到十万量级 / s。从线程这个角度来看,部分原因在于:

  • 多线程带来线程上下文切换开销,单线程就没有这种开销;

  • 锁;

当然 “Redis 很快” 更本质的原因在于:

Redis 基本都是内存操作,这种情况下单线程可以很高效地利用 CPU。而多线程适用场景一般是:存在相当比例的 IO 和网络操作。

总的来说,应用情况不同,采取多线程 / 单线程策略不同;线程池情况下,不同的估算,目的和出发点是一致的。

至此结论为:

IO 密集型 = 2Ncpu(可以测试后自己控制大小,2Ncpu 一般没问题)(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)

计算密集型 = Ncpu(常出现于线程中:复杂算法)

当然说法一中还有一种说法:

对于计算密集型的任务,一个有 Ncpu 个处理器的系统通常通过使用一个 Ncpu + 1 个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个 “额外” 的线程,可以确保在这种情况下 CPU 周期不会中断工作)。

即,计算密集型 = Ncpu + 1,但是这种做法导致的多一个 CPU 上下文切换是否值得,这里不考虑。读者可自己考量。

勿删,copyright占位
分享文章到微博
分享文章到朋友圈

上一篇:微擎小程序安装教程

下一篇:图像分类(Image Classification)经典框架论文翻译汇总

您可能感兴趣

  • Yolo系列详解

    零、图像基本概念 图像表示为二维的矩阵  灰阶图像 0-255,0表示黑色,255表示白色,其余表示灰色。 图像的坐标轴   彩色图像  注意:颜色信息对于任务有时候有用,有时候没用。 一、什么是目标检测 目标检测是计算机视觉中的经典的原子问题,即通过输入的图像来完成物体的检测,它需要解决两个问题: 物体在哪里 物体是什么 目标检测算法的传统实现 sift hog 等算法。这些算法的...

  • grafana使用MYSQL数据源展示

    新增数据源 Grafana支持许多不同的数据源。每个数据源都有一个特定的查询编辑器,该编辑器定制的特性和功能是公开的特定数据来源。 官方支持以下数据源:Graphite,InfluxDB,OpenTSDB,Prometheus,Elasticsearch,CloudWatch和KairosDB。 每个数据源的查询语言和能力都是不同的。 你可以把来自多个数据源的数据组合到一个仪表板dashbo...

  • Unity渲染教程(九):复杂材质 https://www.jianshu.com/p/5e3af869870f

    Unity渲染教程(九):复杂材质 https://www.jianshu.com/p/5e3af869870f 同样的着色器,不同的贴图 用户界面 到目前为止,我们一直都为我们的材质使用Unity默认的材质监视器,它很耐用,但是Unity的标准着色器长得非常不一样。仿照着标准着色器,让我们一起来为我们自己的着色器创建一个自定义监视器吧。 我们默认的监视器和标准着色器监视器 着色器GUI 我...

  • 打家劫舍系列: 198.打家劫舍 213.打家劫舍2 337.打家劫舍3

    198.打家劫舍1 线性的dp,终于可以用到我自己的思路了,f[i]表示偷第i家所能得到的最大金币,那么它必然不能偷第i-1家,即从f[i-2]转移过来,但是我也可以不偷i-2,所以可以从f[i-3]转移过来,但是我不能不偷i-3了,因为中间隔了3个的话,必然能在中间再偷一个,(假设数据都是正数),所以自然而然想到转移方程 f[i]=max(f[i-2],f[i-3])+nums[i-3];...

  • 美团数据库运维自动化系统构建之路

    本文整理自美团点评技术沙龙第10期:数据库技术架构与实践。 美团点评技术沙龙由美团点评技术团队主办,每月一期。每期沙龙邀请美团点评及其它互联网公司的技术专家分享来自一线的实践经验,覆盖各主要技术领域。 目前沙龙会分别在北京、上海和厦门等地举行,要参加下一次最新沙龙活动?赶快关注微信公众号“美团点评技术团队”。 本次沙龙主要围绕数据库相关的主题,内容包括美团数据库自动化运维系统构建、点评侧My...

  • K8s CNI网络最强对比:Flannel、Calico、Canal和W

    介 绍 网络架构是Kubernetes中较为复杂、让很多用户头疼的方面之一。Kubernetes网络模型本身对某些特定的网络功能有一定要求,但在实现方面也具有一定的灵活性。因此,业界已有不少不同的网络方案,来满足特定的环境和要求。 CNI意为容器网络接口,它是一种标准的设计,为了让用户在容器创建或销毁时都能够更容易地配置容器网络。在本文中,我们将集中探索与对比目前最流行的CNI插件:Flan...

  • 美团点评移动网络优化实践

    本文根据第16期美团点评技术沙龙“移动开发实践(上海站)”演讲内容整理而成。 第18期沙龙:高可用系统背后的基础架构(3月25日)火热来袭!快快点击报名吧。 网络优化对于App产品的用户体验至关重要,与公司的运营和营收息息相关。这里列举两个公开的数据: “页面加载超过3秒,57%的用户会离开。” “Amazon页面加载延长1秒,一年就会减少16亿美金营收。” 在做网络优化前,我们首先要为网络...

  • C++基础知识(四)-友元和运算符重载

    整理码字不易,养成好习惯,点赞关注,你的支持就是我写下去的动力,谢谢老板。 本文为C++第三篇后续接着这篇文章写,大家可以持续关注,前三篇在主页 4.5 友元 类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员,怎么办? 解决方法是使用友元函数,友元函数是一种特权函数,c++允许这个特权函数访问私有成员。这一点从现实生活中也...

华为云40多款云服务产品0元试用活动

免费套餐,马上领取!
CSDN

CSDN

中国开发者社区CSDN (Chinese Software Developer Network) 创立于1999年,致力为中国开发者提供知识传播、在线学习、职业发展等全生命周期服务。