4

撬开多线程的大门——学习多线程必须掌握的基本概念

 1 year ago
source link: https://www.cnblogs.com/green-jcx/p/16791600.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

撬开多线程的大门——学习多线程必须掌握的基本概念

进程的概念从字义上理解相对还是比较抽象的,但进程实际上对我们并不陌生,可以说它无时不刻的伴随着我们的生活。当你每天上班打开电脑,运行微信与好友通讯、运行浏览器阅读网页新闻等,这一些将程序运行起来的操作,都属于创建了一个进程。并且我们可以对同一种程序重复运行多次,这意味着一个程序可以创建多个进程,例如我们时常针对Word这一种程序,反复的运行从而阅读不同的文档。

根据我们日常生活中对程序使用的场景而言,我们可以通俗的将进程理解为:进程就是运行起来了的程序;进程是程序的一段执行过程;进程是一个正在执行的程序;进程是程序的实例。程序是静态的,通过运行程序就会产生动态的进程。总之,诸如此类。

正式地说,进程是一个操作系统级别的概念,进程是源于一个具有独立功能的程序,与指令、数据集合的一次运行活动。它是操作系统动态执行任务的基本单元,是操作系统进行资源分配的基本单位。在这一点上就类似于军事战役,司令就像操作系统,它不会对某个士兵下达命令或分配物资,而是以部队为单位下达命令并分配物资,调度各种部队来指挥作战,这里部队的调度分配就有点类似于进程。

从结构上,我们可以想象操作系统是间大房子,众多程序运行同在一件大房子里,如果没有隔离的房间,势必会错乱不堪。而进程会起到类似“房间”隔离的作用,让操作系统的运行环境更加稳定,即使一个程序失败也不会影响另一个程序。从这一点上,我们可以认为,进程提供了程序执行的独立环境和安全边界。

正在运行的操作系统(你现在的电脑)就是由各种进程的活动构成,你可以打开“任务管理器”,可以了解你当前计算机的所有进程,以及进程的资源分配情况。

722260-20221014143354958-1855279351.png

根据上文中的介绍,总而言之,我们可以将进程看作是一个正在运行的程序。既然是运行的程序,必定会对程序有所期许(指示/任务)。试想下,你打开某个程序使它运行是为了什么,你如果你打开“QQ音乐”肯定希望它播放一首你喜欢的歌曲,你如果打开“饿了么”你肯定你希望点一份外卖。对于以上这些,你对程序下达的“指示/任务”,实际上投射到程序当中,就会对应产生一条线程。这一点可以说明,线程是进程运行过程中执行的任务。

一个程序的运行对于用户而言,往往感知不到代码执行的存在,用户通常实现某个功能就点击相应的按钮。实际上,在点击按钮的背后,进程不光会产生一个线程,线程会根据对应的操作选择执行一条代码的路线,通过执行这条代码路线来实现相应的功能。

我们可以将充斥代码的程序想象成一幅旅游地图,地图上不同的旅游线路就像代码中不同的分支,我们选择不同的旅游线路就相当于选择不同的代码分支,通过不同的线路就可以到达不一样的景点。程序的执行也是如此,用户选择执行不同的操作,进程中就会创建不同的代码执行路线,线程会根据相应的路线执行代码,从而实现相应的功能。所以从执行层面,我们可以将线程理解成一条代码的执行路径

722260-20221014143455455-1140444786.png

每一个线程都运行在一个操作系统的进程中,因此进程可以看作是线程的容器。每一个进程都必定包含一个用做程序入口点的主线程,该主线程会在程序运行起来时自动创建。除主线程之外,每个进程还可以通过编程方式创建额外的次线程(工作者线程),无论是主线程还是次线程都属于进程中的一个独立执行单元,并且在多个线程之间它们能够同时访问进程中的共享数据。


让我们回溯到计算机CPU发展早期的时候,那时计算机的CPU都是单核的,并且一个单核的CPU在某一时刻只能执行一个线程。如果需要执行其他的线程,CPU只能等待当前线程执行结束之后,才能执行下一个线程。也就是说你打开了一个音乐程序(进程)播放周杰伦的“七里香”(线程),如果还需要打开记事本程序进行打字,则必须要等到“七里香”这首歌播放结束后才能进行。

上述的等待是CPU在忙着播放美妙的音乐,可能部分人还愿意接受,可是某些等待是无意义的。例如,你将移动硬盘的资源拷贝到计算机的硬盘时,计算机干活的重心会转交给硬盘,它会发出大量I/O指令进行读写操作。然而读写操作通常是比较耗时的,如果CPU想要在这时进行其他的任务处理,则必须要等待硬盘操作完成后才能进行,这就导致CPU经常处于空闲状态

上述说明了CPU在早期发展时的不足之处,于是人们为了满足多应用同时使用的需求,为了提高CPU利用率,从而研究出了一种CPU并发工作的方式。计算机的很多概念,其实都可以在生活中找到影子,并发也是如此。想想你在工作时,一边听着音乐一边打字的样子;想想你在午餐时,一手拿着手机一手拿筷子往嘴里塞事物的样子;以上的这些现象就属于并发,即在同一个事物,在同一个时间阶段内,开展多项任务。

下面来说说并发的工作方式。我们可以将CPU执行的任务看作是线程,一个单核CPU在同一时间阶段内,开展多个线程处理,就体现出了并发。具体来说,操作系统会使用一种算法对线程进行调度,促使将一个CPU的资源可以合理地分配给多个线程(任务),其中每个线程都将分配一段,CPU为其执行的时间片,CPU会在多个线程之间不断切换轮流的执行多个线程,也就是这个任务根据分配的时间片执行一会儿(10ms),在切换到另一个任务根据相应时间片再执行一会儿(10ms)。下图展示了两个线程并发执行的过程:

722260-20221014143644690-1778124886.png

由于并发的方式促使线程切换速度很快,所以并发的执行通常对于用户的感觉而言,就像是多个任务并行一样,以致于产生了一种多个任务同一时间执行的假象。这一点听上去可能有点是是而非,你需要将并发的多个任务看作是在同一个时间阶段内执行的,而非是某个具体的时间点同时执行的。例如,两个任务都是在0到60秒这个阶段中完成的,但如果比较具体的执行时间点,一个任务某个执行时间点是08:30:12,另一个任务某个执行时间点则会是08:30:56。说白了就是,并发就是“一心二用”。这是因为单核CPU的计算机并没有能力在同一时间点运行多个线程。

并发的体现源于单核CPU资源合理的分配,促使多个线程在同一时间阶段内开展,并且有效避免了CPU被某个线程长期霸占的问题,提升了CPU资源利用率。但是,这种方式依然存在弊端。如果单核CPU处理的线程过多,CPU则会花费大量时间在这些线程之间进行切换,这会导致程序的性能下降。


基于单核CPU的短板,并随着计算机硬件的发展,CPU迈入了多核时代,双核、四核、八核已屡见不鲜,甚至还有高达几十核的CPU。多核CPU不在局限于和单核CPU那种“一心两用”的工作方式,而是可以真正实现同一时间点执行多个线程(任务),达到“双管齐下”的效果。多核CPU的每个核心都可以独立地执行一个线程,并且多个核心之间不会相互干扰。因此,多核CPU在不同的核心上,在同一时间点,分别执行一个任务的这种方式,称之为并行。

例如,同样是执行两个任务,双核 CPU 的工作状态如下图所示:

 

722260-20221014143745538-780209578.png

通过上图我们可以看出,双核CPU可以在同一个时间各自执行一个任务,和单核CPU在两个任务之间不断切换相比,它的执行效率更高。需要注意的是,上图中CPU的核数与线程(任务)数刚好匹配,这是个理想状态,如果线程数大于了CPU的核数,那么计算机会按照什么样的方式执行呢?你可以思考一番,然后在下文中找到答案。


5.并发&并行

在实际的情况中,我们的计算机或智能手机,通常都会同时处理几百上千个线程(任务),并且对于目前的硬件条件而言,CPU具备的核数还无法普及或达到一个非常高的数值,所以线程(任务)数大于CPU核数是一个常态化的现象,对于这一点你可以打开任务管理器,通过查看CPU线程数就可以证实。

722260-20221014143908596-83764576.png

所以对于现实中存在的这种情况(核数低于线程数),计算机这个时候对于线程的处理,会同时存在并发和并行两种情况:所有的CPU核心都会并行工作,其中每个核心还会进行并发工作。例如一个双核 CPU 要执行四个任务,它的工作状态如图所示:

 

722260-20221014143933767-593382044.png

上图中每个核心并发执行了两个线程,两个核心并行就执行了四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,具体的分配还是要取决于操作系统的调度算法,以及每个线程的处理状态。

小结

并发的工作方式,会在单核CPU处理多个线程时出现,它代表了单核CPU交替执行不同线程的能力。并行的工作方式,只会在多核CPU处理的硬件条件下,并且线程数与核数相等情况下出现,它代表了多个核心同时执行多个任务的能力。在多核CPU中,并发核并行通常都会同时存在,两种工作方式的结合,会有效的提升计算机执行程序的效率。


6.概念类比

不要被枯燥、笼统的概念所吓跑。以上讲解的知识点都属于计算机的基本概念,初学者往往在读完这些概念后都会感觉比较模糊,这是正常的。其实计算机中大部分的概念,都可以通过生活中的场景进行类比。上文中讲解的概念也是如此。所以,接下来我将基于上文中的进程、线程、并发、并行的概念一起串一串,将它们融入到生活场景中进行类比。让这些概念可以形象化的展示在你面前。

进程

我们首先将应用程序看作是一家饭店。饭店通常在没有客人光临之前,都是一个相对静止的状态,这一点也和未启动的程序一样。当某个家庭来到饭店解决晚餐时,此时静态的饭店开始张灯结彩的欢迎客人了,此情此景可以看作一个程序开始启动了,而这个家庭来到饭店进行晚餐的活动,就类似于程序开启了一个进程

线程

程序通过开启进程活动了起来,那么活动的进程中必定会开展相应的任务。这就等于你饭店在安置后客人就坐后,需要为客人烹制美味菜肴。我们来看看这个家庭点的菜肴:鱼香肉丝、湖北藕汤、剁椒鱼头、夫妻肺片。这些菜肴的制作通常对于一个标准化的饭店而言,都会在厨房中会划分不同的制作区域。例如,鱼香肉丝要在灶台区域、剁椒鱼头要在蒸柜区域。

当厨房要烹制某个菜品时,厨师就会根据菜肴的制作类型(炒、汤、蒸),到达指定的区域进行烹饪。对于这个现象,就像程序为实现不同的功能,会选择不同的代码路径执行一样。不同的菜肴要找到相应的区域进行烹制,并且为客人制作菜肴是饭店的主要任务,综上所述,我们可以将饭店为客人制作菜肴的任务,看作是进程中执行的线程。

 

722260-20221014144032433-826652450.png

并发

先别着急流口水,在点菜之后,我们将目光转向厨房。此时的厨房只有一名厨师在岗,所以他一个人将面临多道菜的制作,这名厨师并不打算一道菜一道菜的制作,因为他担心如果上菜太慢会导致:1.客人在吃前几道菜时就饱了,客人会放弃后面的菜;2.由于菜的间隔时间过长,最后一道菜上时第一道菜就已经凉了。所以他打算一个人先同时开展两道菜的制作,于是他在藕汤进行煨煮时,起锅烧油去锅里炒鱼香肉丝;当鱼香肉丝烧至入味时,去给藕汤进行调味,利用两个菜肴的空挡不断切换,最后当鱼香肉丝出锅时,藕汤也已经煨好了。以上这名厨师同一时间阶段内做多道菜的方式,就类似于与并发。

并行

此时老板来到厨房,看到你非常卖力的为客人准备菜肴,加上客人也有点着急,于是老板穿上了白大褂,戴起来高帽子,打算加入你的行列。此时的厨房就已经有两名厨师了,此时客人还只剩两道菜(剁椒鱼头、夫妻肺片)没有上。显然目前最佳的制作方式就是,两个厨师同时进行菜肴的制作,每个人负责一道菜肴。那么对于以上两名厨师同时进行菜肴的工作方式,就类似于并行。

并发&并行

在刚刚完成好上一桌客人的菜肴制作后,此时饭店又来了一桌客人,由于这桌客人聚餐的性质是公司聚餐,所以这桌客人点的菜肴有十几道菜。此时的厨房只有两名厨师,这就产生了一种情况:菜肴数大于厨师数,这也和线程数大于CPU核数同理。所以此时饭店的最佳工作方式就是,让两名厨师同时做菜,并且一个人负责多道菜的制作。对于这种情况,就类似CPU同时使用并发加并行两种方式开展任务。


 7.多线程编程

多线程的实现可以从硬件或软件上体现。在硬件上,计算机基于单核CPU的并发或多核CPU并行的工作方式,并结合操作系统的线程调度程序,就可以实现多线程处理。在软件上,应用程序可以使用编程语言实现多线程的编码,从而实现在一个进程中创建多个线程,来完成一个程序中多项任务的同时处理。

目的

使用多线程的目的是为了同步完成多项任务。你可以试想下,你正要筹备一场年夜饭的食材。如果采购各式各样的食材全都是你一个人去完成,那么年夜饭的准备时长和开饭时间必定会延长。如果你安排你的家人进行协作,那么你的家人可以和你同时去购买不同的食材,这样一来会有助于节省你购买食材的时间。在这个例子中,你安排家人协作你购买食材,实际上就和编程中使用多线程的目的是一致的,编程中通过多线程会助于改善程序的总体响应性。

切换

多线程是把双刃剑,不是越多越好。每一个线程都需要分配独立的堆栈空间(耗费内存,如一个线程约占用1MB堆栈空间)。并且CPU对线程的切换需要保存很多中间状态、数据等,所以单个进程中的线程过多的话,性能反而会下降,CPU需要花费不少时间在各个线程之间来回切换,以致于耗费本该属于程序运行的时间。

共享

由于一个进程中所有的线程可以获取内存的共享数据,所以多个线程在同时访问某个数据时,会出现数据异常、不一致的情况。这可能会使程序发生非常奇怪、难以发现的bug,而且这些bug难以重现和调试。例如,线程A要写一块数据,同时线程B也要写这块数据。此时就需要采取一定的技术手段,让线程有先有后地去写,而不能同时去写,如果同时去写,可能写进去的数据就会出现互相覆盖等数据不一致的错误。

执行

从编码的角度来看,线程的执行仿佛是我们调用相应的函数来完成的,但实际上并非如此。我们调用线程执行的代码,并不会在程序执行这段代码时立即执行。准确的说,这段调用线程执行的代码,仅仅是通知操作系统尽快地执行这个线程。线程具体的执行,是由操作系统,根据线程调度程序的分配机制决定的。


本文的基本概念只能作为多线程学习的一个开端,后续我将持续产出针对多线程应用的知识。想要将多线程技术更好的运用起来,可谓是,“路漫漫其修远兮”。多线程的技术熟练运用,不光是高级开发人员与中级开发人员之间的一道分水岭,它还是很多实际项目必须采用的一种技术方式。项目不单单只满足于功能而已,对于运行效率的提升,多线程技术的涉猎是不二法则。

戒骄戒躁,千万不要急于求成,不要以为多线程是一个很小的话题。多线程其实是一个很大的话题,请各位读者要稳扎稳打,一步一个脚印地把多线程学好,这会终身受益。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK