当前位置: 首页 > 科技观察

多线程开发中的线程数设计问题

时间:2023-03-15 17:17:21 科技观察

前言我们用几篇文章系统的讲了Java并发编程模型中的一些基础知识。如同步、锁、原子操作、信号量等,它们的一些扩展实现了闩锁、门锁等。今天我们回过头来简单谈谈并发编程模型的设计和选择。主要涉及到在多处理器或者多核时代,我们如何使用多线程设计来提升我们程序??的性能。谈谈线程与应用程序的关系,以及我们在编程过程中如何设计多线程模型。是不是我们在编程的时候设计的线程越多,应用程序的性能提升就越大,在什么情况下设计什么样的线程模型。并发与并行在具体讲多线程并发模型的设计之前,先简单理清两个概念,并发(Concurrency)和并行(Parallel),这是两个容易混淆的概念。并行是指在运行时同时并行执行多个进程或线程。并发是指多个线程一起工作来完成某个任务。可以在单核处理器上通过分时运算来实现,也可以在多核处理器上通过并行运算来实现。因此,并发侧重于编程层面的概念,而并行则指的是运行方式的概念。并行性可以在进程级别,多个进程同时运行,也可以在线程级别,多个独立线程并行运行。强调的是同时性。我们通常所说的并发编程,其本质就是通过多个线程将要执行的任务分解,使之成为多个可以独立执行的小任务,从而实现在单核上分时执行或者并行执行在多核上。实施。从而减少整体任务的处理时间,从而提高应用程序的性能。多线程并发编程模型在编程领域,并发编程设计实际上是相对于顺序同步编程或串行编程设计而言的。序列化编程就是把任务排队,只让一个目标处理器运行它,而且是单线程处理的数量。要提高这类应用的性能,我们只能提高处理器的执行效率,但它的提高是有限的,毕竟单核的处理能力是有上限的。当我们的计算机进入多核时代后,串行编程中使用的单线程模型就不能使用多核了。因此,我们将应用程序的执行任务分解成可以独立执行的小任务,交给多个线程,让它们在多个内核上并行执行。多个处理器或内核并行执行多个线程,可以充分发挥并发编程模型的威力,缩短应用代码的执行时间。这就是为什么当我们进入多处理器或多核计算机时代时,多线程编程已成为提高应用程序性能的重要选择。因为这样的设计可以给我们带来很大的应用性能提升,同时也增强了我们应用的响应能力。当然,单核时代的并发编程模型也能在一定程度上提高应用程序的效率,因为并发编程模型会将一个大任务分解成许多小任务,由多线程负责。利用处理器的分时处理机制,有效地利用了任务执行过程中产生的等待时间。多线程应用的分类,正是因为进入多核时代后,每个可用的CPU核都可以独立处理自己的任务,真正实现了运行时的并行。因此,在并发编程模型中,我们将一个大任务分解成一系列独立运行的小任务交给多个CPU核心并行执行,这确实大大提高了整个应用程序的整体运行效率。一般来说,我们在设计多线程并发编程模型时,首先会考虑对我们的应用程序进行一个简单的分类,即识别它是计算密集型程序还是I/O密集型应用程序。因为这两类程序在CPU上执行时有很大的不同,如果是计算密集型程序,其处理主要集中在从寄存器中读取数据进行计算,然后写入寄存器的过程。由于寄存器缓存的读写速度更接近CPU的内部缓存,所以CPU的计算时间可以得到很好的利用,无需等待和上下文切换。对于IO密集型应用,由于涉及到大量的输入/输出操作,而且这些操作大多由专门的输入输出设备来处理,因为它们的处理速度与CPU的处理速度相差太大,浪费了CPU过多的等待时间。为了充分利用目前多处理器或多核的计算能力,使用多线程并发设计编程来提高应用程序的性能是我们唯一的出路。下面用例子来说明上面两类应用的设计:folderontheharddisk对于??大大小小的应用程序,既然我们可以一次性将相关数据读入到处理内存中,那么接下来重要的就是对数据进行计算,计算量很大。如果采用单线程方式实现,单线程方式需要遍历所有文件,依次对每张图片进行缩放。使用这种单线程模型,即使我们有一个多核CPU,调整大小过程也只会使用一个可用的CPU核。但是如果采用多线程的方式,可以定义一个主线程负责扫描文件系统,并将所有找到的文件添加到一个队列中,由一组工作线程来处理。那么这个时候我们就需要注意了,这种情况下我们的多线程处理模型应该怎么设计呢?我们简单的想一想就知道,计算过程最好是将每个CPU绑定到一个特定的线程上,不断的读取数据进行处理就可以了。因此,如果我们恰好拥有与可用CPU内核一样多的工作线程,那么我们可以确保每个可用的CPU内核在处理图像时都有事情要做。充分利用计算机的计算能力,不存在线程上下文切换的问题,不存在等待输入输出的问题,从而缩短图片的处理时间,提高整个应用的性能。也就是说,在设计我们的并发模型的时候,如果主要涉及到数据计算的问题,我们可以设计与可用CPU核数相同的线程数来绑定执行线程的CPU核数,以充分利用空闲的CPU。内核处理能力。在这种情况下,如果我们考虑可扩展性,我们可能希望通过增加更多的资源来提高性能,这只能从增加单个CPU的计算能力的角度来实现。如果在这个例子中,我们遇到需要处理的图片数量较多,是否可以在我们的应用程序中增加更多的线程来进行处理呢?其实这种计算密集型的应用,由于我们现在机器的CPU核数有限,增加更多的线程并不能提高性能。相反,因为负责调度的线程必须管理更多的工作线程,而线程的创建和关闭也会消耗CPU,添加的线程越多,性能反而会下降。I/O密集型应用多线程设计对于输入/输出密集型应用,即I/O等待时间大的应用,如何设计使用多线程来提高应用的整体性能?让我们想象一个例子。假设我们要编写一个应用程序,将一个完整的网站内容以HTML文件的形式镜像到硬盘上。因为涉及到网络访问,需要进行大量的网络I/O操作。因此,它是一个典型的I/O密集型应用程序。具体的执行过程是怎么做的?我们需要从这个站点的每个页面开始,搜索每个站点中的链接,然后根据这些链接向它的web服务器发送访问请求。由于涉及到网络请求,请求的响应时间是不确定的,一个请求可能需要很长时间才能收到响应。同样,如果我们使用单线程的处理方式,等待回复的时间可能是一个难以忍受的过程。而在这个等待期间,我们的应用可能什么也做不了。如果我们可以将这项工作分散到多个线程中,让一个或多个线程负责解析请求收到的HTML页面并将找到的链接放入队列中,而其他线程向Web服务器发出请求并等待回复.这样我们的应用程序就可以在新请求页面的等待时间内解析已经接收到的页面。而我们知道网络传输涉及的输入输出过程都是由我们的操作系统网卡来处理的,也就是说我们应用的线程只需要负责发送请求,然后等待远程网络回复即可。在这段时间里,线程可以不被阻塞等待,去做其他的事情。同时,由于我们的CPU基本要做的就是响应输入输出操作的开始和结束指令,以及做一些访问和存储线程的处理工作,所以大部分时间应该是响应事件处理。这时候我们CPU的可用核心处理过程不需要线程绑定,所以当这类IO操作密集型应用多线程的时候,我们可以在应用中添加多于可用CPU核心数的线程来充分利用其计算能力,那么应用程序甚至可能获得更好的性能。简而言之,应用程序性能意味着在更短的时间内完成更多的工作。再来看另一种情况。在我们的图形用户界面(GUI)应用中,经常会遇到需要用户输入一些内容,然后单机处理按钮将数据提交给服务器进行处理的操作过程。在这个过程中,当我们在单机上按下按钮时,如果是单线程处理,应用就会被阻塞,等待服务器返回处理结果。这个时候我们一般会把按钮设为不可用,目的是为了防止用户重复提交。服务器在后台处理数据,用户只能等待服务器回复结果。这样的用户体验会非常糟糕。如果处理时间稍长,鼠标不能移动,可能会给用户造成不对劲卡住的错觉。这时候我们可以使用多线程来处理,即设计一个额外的线程运行并等待远程服务器处理结果,同时当前处理线程继续响应其他用户操作请求。该线程负责在远程回复到达时进行响应。在这类程序中使用多线程会给用户带来良好的操作体验,并大大提高应用程序的相应能力。总结这里我们简要总结了如何在现代多处理器或多核环境中通过多线程并发设计来提高我们的应用程序的性能和响应能力。需要注意的是,在设计的时候,首先要确定我们应用的类型,是计算密集型还是I/O密集型。如果是计算密集型应用,那么我们设计的线程数应该等于我们可以使用的CPU核数,反之,如果是I/O密集型应用,我们可以将线程数设置的大很多比可用CPU内核的数量更能提高性能。当然,所有的多线程并发编程模型都离不开竞争资源的处理,这就需要我们充分理解同步、锁、原子操作、信号量以及各种派生的闩锁和门锁等概念。精通设计流程,保护种族资源。