多线程与并发编程

距离上次更新已经过了很久了,最近一直在弄公司的三个新产品,目前也终于告一段落了。

目前的生产环境系统,CPU性能基本都是过剩的,如何提升系统的性能与使用率呢? 压榨CPU的性能就很有必要了,这里我们先一步步的来看看如何提升CPU使用率。

基本概念

进程

在说线程之前,我们先来说说进程,学过操作系统的小伙伴的都知道进程是OS分配资源的最小单元,一般由程序,数据集合和进程控制块三部分组成。

这里讲起来还是比较抽象,举两个例子:

1. 电脑打开一个应用程序,会启动一个进程,再打开一个应用程序,会再启动一个进程;

2. 微服务与单体服务不同模块的根本区别是什么?一个微服务是一个进程,单体服务的不同模块是同一个进程,微服务之间的交互是跨进程交互。

线程

线程是程序执行的最小单位,进程是线程的容器,不同的线程公用进程的资源,那么线程又如何使用CPU资源呢? 有哪些状态呢?

1. 线程创建后,会进入就绪状态;

2. 就绪状态线程在获取到CPU资源时会变更为运行状态;

3. suspend(),sleep()方法被调用,使用wait()来等待条件变量,线程处于I/O请求的等待时,线程会从运行进入阻塞状态;

4. 线程收到stop()指令时,会进入死亡状态,而运行时线程还可通过destroy()指令进入死亡状态;

5. 阻塞状态线程会在收到notify(), notifyAll(), resume(), I/O执行完成后,恢复就绪状态;

6. 运行状态线程会在时间片运行完成后,变为就绪状态,这里关于时间片有兴趣了解的可以看看OS相关资料。

那么我们在这里留两个小问题:

1. 线程与CPU核数有什么关系?这会涉及到多线程场景下一些问题;

2. 一个系统用多少线程合适?

纤程

线程是OS级的,纤程是线程中的线程,切换和调度不需要经过OS(操作系统)。其资源占用更少(一个线程大约为1M,一个纤程大约为4K),切换更轻量更快(不需要OS调度)。

天然支持纤程的语言有Go、Scala、Kotlin,JavaQuasar库支持纤程,当我们需要进行大量快速计算时,使用纤程会在性能上领先线程。

基础知识

这里我们还要再补充一些基础知识,这些知识会包括一些计算机组成原理的内容。如果对这块比较了解或没兴趣的都可以跳过。

我们在这里以一个游戏举例,我们回忆一下这样的场景(以阿婆主中学时的例子举例吧):

1. 阿婆主在电脑里安装了暗黑破坏神2,占用了硬盘空间2G,计算机内存64M,CPU缓存64K(似乎那时还没有多级缓存?);

2. 安装好后迫不及待点击运行.exe,于是系统进入loading,这时计算机会从硬盘把部分内容加载进内存,硬盘咔咔地响;

3. 进入界面后做一些点击,发现硬盘灯没有闪闪闪,整个操作也响应很快,说明没有发生与硬盘的数据交换,数据在内存与寄存器中,这里的速度会比硬盘快;

4. 跑到野外,来了一片小怪,放了一个技能,原本流畅的运行开始卡了,但是硬盘依然没有咔咔响,这时候可能是什么呢?大概率就是内存与寄存器及CPU缓存在做数据交换,因为缓存速度又是内存的100倍。我放了一个技能集中一片小怪,CPU要做大量运算,这些运算需要不同数据,可能是多线程在运行,因此会发生大量的线程切换以及缓存内存数据交换(缓存无法命中数据时,会去内存获取)。

以上这里涉及到了磁盘、内存、CPU(缓存、寄存器组、计算单元),CPU的每个核都包含自己的缓存、寄存器组、计算单元,而要提升系统的性能,我们就要去思考如何更好利用这些组件,这里我们大概看下来是不是应该让数据尽量多的从缓存获取(寄存器是存放计算的数据,缓存存放挂起的线程的中间数据,缓存不足时较早的数据会被挤出只得再从内存获取)。

那么这里又引出了另一个问题,大家思考一下,一个CPU核会有自己的计算单元与缓存,那么如果一组数据存在于一个64bit(缓存长度),不同核心分别进行了该数组的值修改,会发生什么情况?对数值与性能有什么影响?超线程如何提升CPU性能?

为什么要多线程

我们看到CPU在等待IO时,也会阻塞,那么这时候我们能否把等待时间使用起来?那就再来一个线程嘛,在上一个线程等待IO时,我来另一个线程把运算资源使用起来,这样是不是就把CPU充分利用起来了。

所以什么是多线程呢?就是在外部看来原本排队执行的工作,现在一起在做了(虽然在只有一个计算单元时,其实内部也是交替在进行),这样等待时间也被利用起来了。

既然多线程这么好,我们直接多线程不就好了,为什么还要单线程呢?这里面有哪些问题可能存在呢?我们要如何规避呢?

多线程的问题

下面我们来看看多线程主要会存在的一些问题,如果不了解这些问题,那么多线程可能就是一个噩梦,会让我们的系统发生完全不可预知的运行结果。

可见性

要理解可见性,我们就要回到前面的基础知识,要去理解内存与CPU的缓存(多级缓存):

1. 当线程将数据读取进缓存并更改,该数据不会更新至其他缓存;

2. 另一个线程使用该数据时,并不知道另一个线程已更改该数据,获取到的依然是原值;

3. 最终在多线程情况下,该数值在每个线程都不一样了。

那要怎么办呢?这个在CPU的底层都已经做了协议,叫缓存一致性协议,这是个硬件级的协议,每个缓存会被标记为modified、shared、exclusive、invalid四个状态,基于该协议系统会在多级缓存之间做数据的同步。

JAVA中的实现:

一个变量默认没有做到可见性,我们可以通过volatile或

版权声明:玥玥 发表于 2021-05-30 12:44:01。
转载请注明:多线程与并发编程 | 女黑客导航