Cing
发布于 2022-06-14 / 286 阅读
0

多线程是如何提升性能的

引言

首先来思考几个问题

  1. 多线程是如何提高程序性能的?
  2. 单核处理器使用多线程能提高性能吗?
  3. 单核处理器多线程会有线程安全问题吗?
  4. 单核处理器为什么要使用多线程?

思考

  1. 第一个问题:通过将一个大任务拆分为多个同类小任务同时执行后汇总结果。性能提升主要依靠 同时多个同类任务

  2. 基于第一个问题,单核处理器在使用多线程时很显然不能满足 同时多个同类任务 这两个条件,那么肯定是不能提高性能的

  3. 处理器是通过划分时间片的方式来处理多线程的(多进程也是),例如 变量 a 初始值为 0,我们对他进行 a = a + 1 的操作,在我们看来 a = a + 1 是一次完成的,但对于 CPU 来说,实际上是分了三步(见下)。假设有两个线程来执行这个任务,那么按照预期,我们希望得到 a = 3 但实际上可能会得到 a = 2,因为 线程 1 执行第一步后 CPU 可能会切换到 线程2 执行第一步,这样最终结果就会是 2 了

    1. 变量 a 读取到某一个 寄存器R 存储
    2. CPU寄存器R的值进行计算
    3. 计算完成后将值存回内存
  4. 那既然单核处理器不能提高性能,还会有线程安全问题,为什么还要使用多线程呢?这是为了多任务处理,例如: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 为处理器核心数,这个公式只是建议值,每种业务场景都不同,瓶颈也不同,并不能在所有场景下获得最佳性能,想获得最佳性能需要结合自身业务场景,进行压测,调整线程数。

连接池

我们来看一个非常常见的业务场景:数据库连接池

我们知道,数据库建立链接是非常消耗系统资源的,通常我们会使用池化技术,创建后不销毁,进行复用,减少资源消耗,提升系统性能

对于连接池线程数量的设置,HikariCPWiki 页面有详细的介绍,其内有 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 等待,则可以适当提高线程数量

另外还有一个反直觉的结论:响应速度越快,线程数量应当越接近核心数量,而不是更多的线程