引言
首先来思考几个问题
- 多线程是如何提高程序性能的?
- 单核处理器使用多线程能提高性能吗?
- 单核处理器多线程会有线程安全问题吗?
- 单核处理器为什么要使用多线程?
思考
-
第一个问题:通过将一个大任务拆分为
多个同类小任务
并同时执行
后汇总结果。性能提升主要依靠同时
与多个同类任务
-
基于第一个问题,单核处理器在使用多线程时很显然不能满足
同时
与多个同类任务
这两个条件,那么肯定是不能提高性能的 -
处理器是通过划分时间片的方式来处理多线程的(多进程也是),例如
变量 a
初始值为 0,我们对他进行a = a + 1
的操作,在我们看来a = a + 1
是一次完成的,但对于CPU
来说,实际上是分了三步(见下)。假设有两个线程来执行这个任务,那么按照预期,我们希望得到a = 3
但实际上可能会得到a = 2
,因为线程 1
执行第一步后CPU
可能会切换到线程2
执行第一步,这样最终结果就会是 2 了- 把
变量 a
读取到某一个寄存器R
存储 CPU
对寄存器R
的值进行计算- 计算完成后将值存回内存
- 把
-
那既然单核处理器不能提高性能,还会有线程安全问题,为什么还要使用多线程呢?这是为了多任务处理,例如:
GUI 绘制
与计算
的同时处理(单核处理器并不是真的同时,只是切换时间在纳秒级别,人类感知不到),倘若这两项不能同时进行,那你的电脑每操作一下都会卡顿住
对于开发意味着什么
单核处理器
如果你的服务器只有单核,就不要期望多线程能提高 计算型任务
的性能了,理论上来说,只能越搞越慢。。。如果是 IO 型任务
倒是能有所提升,快出来的时间是等待 IO 响应的时间,例如网络请求
多核处理器
多核处理器使用多线程可以提高 计算型任务
与 IO 型任务
的性能,当然其中有一个很大的难点就是线程数量如何设置
开发人员有一个常见的误区,就是认为线程数量越多,性能越好,实际上并不是如此
因为超出核心数量的线程实际上都是在共享 CPU 时间片
,性能只会更差,当然这是针对与 计算型任务
,对于 IO 型任务
往往给出更多的线程
为什么 IO 型任务
用更多的线程可以获得更好的性能呢?
例如有两个外部服务 服务 A
和 服务 B
,均通过 HTTP
接口对外提供服务,他们的响应时间均为 10s,我们的服务需要拿到两个服务的结果再进行一些操作
假设我们的服务使用的是单核处理器,串行(单线程)访问两个服务
先请求 服务 A
使用了 10s,再请求 服务 B
使用了 10s,对两个结果处理用了 5s,共计 25s
那么如果使用多线程呢?
处理器请求了 服务 A
后不等待结果返回,继续去请求 服务 B
,10s 后收到了两个服务的返回,对结果处理花费 5s,共计 15s
可以看到,在 网络 IO
占用了整个链路大量时间的情况下,即便是单核处理器也可以通过多线程来获得性能提升
有限的资源
那么按照上面所说,我们给 CPU 型任务
设置线程数为核心数即可得到最佳性能(根据一些实践 n + 1 线程数能获得更好的性能,n 为处理器核心数量)
那么 IO 型任务
呢?我们的程序大部分都是 IO 型任务
,很少有纯计算的任务,主要业务都在 CRUD(增查改删)
,这中任务如何设置线程数呢?
PG
的工程师们经过测试,给出了通用公式 2n + 1
n 为处理器核心数,这个公式只是建议值,每种业务场景都不同,瓶颈也不同,并不能在所有场景下获得最佳性能,想获得最佳性能需要结合自身业务场景,进行压测,调整线程数。
连接池
我们来看一个非常常见的业务场景:数据库连接池
我们知道,数据库建立链接是非常消耗系统资源的,通常我们会使用池化技术,创建后不销毁,进行复用,减少资源消耗,提升系统性能
对于连接池线程数量的设置,HikariCP
的 Wiki 页面有详细的介绍,其内有 Oracle
工程师做的测试视频及 PG
工程师提出的通用公式(前文提到的)
我这里其实是对连接池为什么能提升效率而引发的一些思考做记录
假设我们的 DB 服务器
是 2C 4G 1Disk
的配置 1Disk
指一个机械硬,业务服务部署在另外的服务器上,即 一台业务服务器
和 一台 DB 服务器
我们知道机械硬盘是靠磁头与盘片的协作来完成数据检索与存储的
那一个硬盘只有一个磁头,即便 DB 服务器
有再多的核心,开启再多的线程,最终都会等待在 磁盘 IO
上,又因为磁头只有一个,最终都是串行的,那么数据库连接池还有什么用?从实践上我们可以得知,确实可以提升效率,那就来思考一下,为什么能提高性能,这不是和我们的推论相悖吗
数据库查询除了 磁盘 IO
以外还有 网络 IO
如前所述,多线程可以减少 网络 IO
所花费的时间,那 磁盘 IO
由于磁盘特性,是否不能减少了呢?
这里要提到一个概念,叫做:程序的局部性原理,其中有一种空间局部性,大意是指短期内的数据访问大都处于同一区域
基于这个概念,DB 通常采用一些特殊读取方式,例如 MySQL 对数据的读取是按照 页(16KB)
这样的概念,一次读取目标数据所属整个页的数据,并且将其加入 buffer pool
,在 buffer pool
中的数据失效之前,就可以直接从内存中取到数据,不涉及 磁盘 IO
,实际上 buffer pool
就是引擎层的缓存,另外提一嘴 MySQL 在 8.0 删除的是 Server 层的缓存,和 buffer pool
不是一回事
处理器对内存的访问可以是同时的(此处存疑,似乎与硬件架构有关)
另外,一条语句发送到 DB 服务器
后要经过 连接器
, 分析器
, 优化器
, 执行器
,这些步骤可以多条语句并行,减少等待时间
至此我们可以有一个粗略的结论:多线程的线程数量最好是与处理器核心数量一致,若执行的任务中有一些 IO 等待,则可以适当提高线程数量
另外还有一个反直觉的结论:响应速度越快,线程数量应当越接近核心数量,而不是更多的线程