Cing
Published on 2022-06-14 / 433 Visits
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 为处理器核心数,这个公式只是建议值,每种业务场景都不同,瓶颈也不同,并不能在所有场景下获得最佳性能,想获得最佳性能需要结合自身业务场景,进行压测,调整线程数。

连接池

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

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

对于连接池线程数量的设置,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 等待,则可以适当提高线程数量

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