如何通过多线程并发设计来提高应用程序的性能

电子说

1.3w人已加入

描述

前言

前面我们用了几篇文章系统的说了一下有关Java并发编程模型中的一些基础的知识。比如同步,锁,原子性操作,信号量等以及它们的一些延展实现闩锁,栅锁等等。

今天我们回过头来简单说一下并发编程模型的设计和选择。

主要涉及到我们如何利用多线程设计来在多处理器或者多内核时代如何提高我们应该程序的性能。

说说线程和应用程序之间的关系,以及我们在编程过程中如何去设计多线程模型。是不是我们编程时设计的线程越多对应用程序的性能提升越大,什么情况下设计什么样的线程模型。

并发与并行

在具体说多线程并发模型设计之前,我们先来简单澄清两个概念,并发(Concurrency)和并行(Parallel),

它们是两个容易混淆的概念,它们的基础都是多线程,而并行是指多个进程或线程之间在运行时同一时间里并行的执行。

而并发则是多个线程一起共同去完成某一项任务。可以在单核处理器上分时运行实现,也可以在多核处理器上并行运行实现。

所以说并发偏重于是编程级别的概念,而并行则多指运行方式的概念。并行可以是进程级别上,多个进程同时运行,也可以是线程级别的多个独立线程的并行运行。强调的是同一时间上的同时发生。

我们通常所说的并发编程,实质就是通过多个线程来分解要执行的任务,使其成为多个可独立执行的小任务,做到可以在单核上分时执行,也可以在多核上并行执行。从而缩短总任务的处理时间,从而提高应用程序的性能。

多线程并发编程模型

在编程领域里,并发编程设计其实是跟顺序同步编程或者串行化编程设计相对的。

串行化编程是将任务排队,针对的运行它的目标处理器只有一个,而且数单线程处理的。要提高这类应用程序的性能,我们只能通过提高该该处理器的执行效率来完成,但是它的提升是有限的,毕竟单核的处理能力是有上限的。

当我们的计算机进入多内核时代后,串行化编程所使用的单线程模型是无法使用多内核的,为此我们将应用程序的执行任务分解成可独立执行的小任务,交给多个线程,让它们在多个内核上并行执行。

多个处理器或者内核并行的执行多个线程,能够很好发挥并发编程模型的威力,缩短应用代码的执行时间。

这就是为什么我们进入多处理器或者多内核计算机时代后,多线程编程成为提高应用程序的性能的一个重要的选项。

因为这种设计能够给我们带来极大的应用程序性能提高,同时还能增强我们应用程序的响应性。

当然,在单核时代并发编程模型也能够在一定程度上提高应用程序效率,因为并发编程模型会将一个大的任务分解成由多个线程负责的众多小任务,在一个单核处理器上借助处理器的分时处理机制来执行,从而有效的利用了任务执行过程中出现的等待时间。

多线程应用程序分类

正是由于进入多核时代后,每个可用CPU内核都可以独立处理自己的任务,真正在运行时做到了并行,所以在并发编程模型中,我们将一个大任务分解为一系列独立运行的小任务,交给多个CPU内核来并行执行,从而真正的大大提高了整个应用程序的总运行效率。

一般情况下,我们在设计多线程并发编程模型时会首先考虑将我们的应用程序进行一个简单的分类,就是去识别是计算密集型程序还是I/O密集型的应用程序。

因为这两种类型的程序在CPU上执行时有很大的不同,如果是计算密集型程序其处理主要集中在从寄存器读取数据进行运算然后写入寄存器这样的过程,由于寄存器缓存的读写速度更靠近于CPU内部缓存,所以CPU的计算时间可以很好的被利用,不需要进行等待和上下文的切换。

而对于IO密集型应用程序来说,由于涉及到大量的输入/输出操作而这些大多是由专门的输入输出设备来负责处理的,由于它们的处理速度跟CPU有太大的差距,造成了CPU过多的等待时间的浪费。

为了能够充分利用现在多处理器或多内核的计算机算力,我们采用多线程并发设计编程来提高应用程序的性能是必由之路。下面我们举例说明上面两种类型应用的设计:

计算密集型应用程序多线程设计

首先看计算密集型应用,这种类型的处理,最常见的例子比如我们想处理硬盘上某个文件夹里的图片大小的应用,由于我们可以一次性将相关数据读入处理的内存,接下来重要的就是对数据进行运算处理了,属于计算密集型处理。

如果是单线程方法来实现的话,单线程方法需要遍历所有文件并依次缩放每个图片。

这种单线程模型下,即使我们有一个具有多个核心的CPU,调整大小的过程将只使用一个可用的CPU内核。

而如果采用多线程方法可以定义一个主线程负责扫描文件系统并将所有找到的文件添加到一个队列中,该队列由一组工作线程来负责处理。

那么这时候就要注意了,这种情况下我们如何设计我们的多线程处理模型呢?

我们简单想一下就知道,计算过程最好是每个CPU绑定特定的线程,不断的读取数据处理即可。

所以如果我们刚好有与可用CPU内核数一样多的工作线程,那么我们就能确保每个可用的CPU内核在处理图片时都有事情可做。

充分利用计算机的算力,也不存在线程上下文切换问题,也不存在等待输入输出问题,从而缩短图片的处理时间,提供整个应用程序的性能。

也就是说在我们并发模型设计时,如果主要涉及到数据的运算问题,我们可以设计跟可以使用的CPU内核数相同的线程数,以绑定执行线程的CPU内核,来充分的利用空闲的CPU内核处理能力。

这种情况下,如果从可扩展性考虑,我们可能想通过添加更多资源来提高性能,那么只能通过提高单个CPU算力的角度来实现。

假如在这个例子中,我们遇到了更大量的图片需要处理,我们是否能够为我们的应用程序添加更多的线程来处理呢?

其实这种计算密集型应用,由于我们当前机器的CPU内核数量有限,添加更多的线程并不能提高性能。

反而由于负责调度的线程必须管理更多的工作线程,并且线程的创建和关闭也会消耗CPU,而会出现增加越多线程性能反而可能下降的现象。

I/O密集型的应用程序多线程设计

对于输入/输出密集型应用程序来说,也就是说就是具有大量I/O等待时间的应用程序,利用多线程提高应用程序整体性能,如何设计呢?

我们来假想一个例子,假设我们想要编写一个应用程序,将一个完整的网站内容以HTML文件的形式镜像到硬盘上。因为涉及到网络访问,需要大量的网络I/O操作。

所以,它是典型的输入/输出密集型应用。

具体实现过程怎么做呢?我们需要从这个站点的每一个页面开始,去搜索其每一个站内的链接,让后根据这些链接向其web服务器发送访问请求,由于涉及到网络请求,所以请求的响应时间就不确定了,某个请求可能需要很长时间才能收到回复。

同样,如果我们采用单线程的处理方式,那么这其中的等待回复时间可能是一个让人无法忍受的过程。而且在这等待期间,我们的应用程序可能啥都做不了。

如果我们可以将这份工作分配到多个线程,让一个或多个线程负责解析请求接收到的HTML页面,并将找到的链接放入队列,而其他线程则向web服务器发出请求,然后等待回复。如此我们的应用程序能够在新请求页面的等待时间里来解析已经接收的页面。

而我们知道涉及到网络传输的输入输出过程都是由我们的操作系统网卡负责的,也就是说我们的应用程序的线程只需要负责将请求发送出去,然后等待远程的网络回复即可,这等待期间线程可以做别的事情,而不必被阻塞等待。

同时由于我们的CPU要做的基本上就是响应一下输入输出操作开始和结束指令,做一些访问和存储线程处理工作,大部分时间应该都是响应事件处理。

此时我们的CPU可用内核处理过程是不需要线程绑定的,所以这类IO操作密集类型应用多线程时,我们可以在应用程序中添加多于可用CPU内核数的线程来充分利用其算力,那么这个应用程序甚至可能获得更好的性能。

简单来说,应用程序的性能意味着能在更短的时间内完成更多的任务。

我们再来看另外一种情况,在我们的图形用户界面(GUI)应用中,我们常常会遇见需要用户输入一些内容,然后单机处理按钮来提交数据给服务器进行处理这样的操作过程,在这个过程中,当我们单机按钮后,如果是单线程处理的情况下,应用程序会被阻塞,等待服务器处理的结果返回。

这时我们一般为了防止用户重复提交而将按钮变成不可用状态,服务器在后台处理数据期间用户就什么也做不了,只能等待服务器回复结果。这样的用户体验会很糟糕,如果处理时间稍长一些,鼠标都无法移动,就有可能给用户造成出问题卡死的错觉。

这个时候,我们完全可以采用多线程来处理,那就是设计一个额外的线程运行等待远程服务器处理结果,而当前的处理线程继续相应用户的其它操作请求。

当远程回复到达时,该线程负责响应。多线程在这类程序中的使用,会给用户带来良好的操作体验,大大提高应用程序的相应能力。

总结

这里我们简单总结了一下,在现代多处理器或多内核环境下,如何通过多线程并发设计来提高我们应用程序的性能和响应性。

需要注意的是在设计时首先要确定我们应用程序的类型,是计算密集型还是I/O密集型,如果是计算密集型应用,那么我们设计线程的数量应该等同于我们所能使用的CPU内核数,反之,如果是I/O密集型应用,我们可以设置远大于可以CPU内核数的线程数来提高性能。

当然,所有的多线程并发编程模型都离不开对竞争资源的处理,这就需要我们充分的理解同步,锁,原子性操作,信号量,以及各种衍生的闩锁,栅锁等概念,熟练的在设计过程中对竞态资源进行保护处理了。

作者:道以致远 来源:今日头条

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分