撬开多线程的大门——学习多线程必须掌握的基本概念
1.进程
进程的概念从字义上理解相对还是比较抽象的,但进程实际上对我们并不陌生,可以说它无时不刻的伴随着我们的生活。当你每天上班打开电脑,运行微信与好友通讯、运行浏览器阅读网页新闻等,这一些将程序运行起来的操作,都属于创建了一个进程。并且我们可以对同一种程序重复运行多次,这意味着一个程序可以创建多个进程,例如我们时常针对Word这一种程序,反复的运行从而阅读不同的文档。
根据我们日常生活中对程序使用的场景而言,我们可以通俗的将进程理解为:进程就是运行起来了的程序;进程是程序的一段执行过程;进程是一个正在执行的程序;进程是程序的实例。程序是静态的,通过运行程序就会产生动态的进程。总之,诸如此类。
正式地说,进程是一个操作系统级别的概念,进程是源于一个具有独立功能的程序,与指令、数据集合的一次运行活动。它是操作系统动态执行任务的基本单元,是操作系统进行资源分配的基本单位。在这一点上就类似于军事战役,司令就像操作系统,它不会对某个士兵下达命令或分配物资,而是以部队为单位下达命令并分配物资,调度各种部队来指挥作战,这里部队的调度分配就有点类似于进程。
从结构上,我们可以想象操作系统是间大房子,众多程序运行同在一件大房子里,如果没有隔离的房间,势必会错乱不堪。而进程会起到类似“房间”隔离的作用,让操作系统的运行环境更加稳定,即使一个程序失败也不会影响另一个程序。从这一点上,我们可以认为,进程提供了程序执行的独立环境和安全边界。
正在运行的操作系统(你现在的电脑)就是由各种进程的活动构成,你可以打开“任务管理器”,可以了解你当前计算机的所有进程,以及进程的资源分配情况。
2.线程
根据上文中的介绍,总而言之,我们可以将进程看作是一个正在运行的程序。既然是运行的程序,必定会对程序有所期许(指示/任务)。试想下,你打开某个程序使它运行是为了什么,你如果你打开“QQ音乐”肯定希望它播放一首你喜欢的歌曲,你如果打开“饿了么”你肯定你希望点一份外卖。对于以上这些,你对程序下达的“指示/任务”,实际上投射到程序当中,就会对应产生一条线程。这一点可以说明,线程是进程运行过程中执行的任务。
一个程序的运行对于用户而言,往往感知不到代码执行的存在,用户通常实现某个功能就点击相应的按钮。实际上,在点击按钮的背后,进程不光会产生一个线程,线程会根据对应的操作选择执行一条代码的路线,通过执行这条代码路线来实现相应的功能。
我们可以将充斥代码的程序想象成一幅旅游地图,地图上不同的旅游线路就像代码中不同的分支,我们选择不同的旅游线路就相当于选择不同的代码分支,通过不同的线路就可以到达不一样的景点。程序的执行也是如此,用户选择执行不同的操作,进程中就会创建不同的代码执行路线,线程会根据相应的路线执行代码,从而实现相应的功能。所以从执行层面,我们可以将线程理解成一条代码的执行路径。
每一个线程都运行在一个操作系统的进程中,因此进程可以看作是线程的容器。每一个进程都必定包含一个用做程序入口点的主线程,该主线程会在程序运行起来时自动创建。除主线程之外,每个进程还可以通过编程方式创建额外的次线程(工作者线程),无论是主线程还是次线程都属于进程中的一个独立执行单元,并且在多个线程之间它们能够同时访问进程中的共享数据。
3.并发
让我们回溯到计算机CPU发展早期的时候,那时计算机的CPU都是单核的,并且一个单核的CPU在某一时刻只能执行一个线程。如果需要执行其他的线程,CPU只能等待当前线程执行结束之后,才能执行下一个线程。也就是说你打开了一个音乐程序(进程)播放周杰伦的“七里香”(线程),如果还需要打开记事本程序进行打字,则必须要等到“七里香”这首歌播放结束后才能进行。
上述的等待是CPU在忙着播放美妙的音乐,可能部分人还愿意接受,可是某些等待是无意义的。例如,你将移动硬盘的资源拷贝到计算机的硬盘时,计算机干活的重心会转交给硬盘,它会发出大量I/O指令进行读写操作。然而读写操作通常是比较耗时的,如果CPU想要在这时进行其他的任务处理,则必须要等待硬盘操作完成后才能进行,这就导致CPU经常处于空闲状态。
上述说明了CPU在早期发展时的不足之处,于是人们为了满足多应用同时使用的需求,为了提高CPU利用率,从而研究出了一种CPU并发工作的方式。计算机的很多概念,其实都可以在生活中找到影子,并发也是如此。想想你在工作时,一边听着音乐一边打字的样子;想想你在午餐时,一手拿着手机一手拿筷子往嘴里塞事物的样子;以上的这些现象就属于并发,即在同一个事物,在同一个时间阶段内,开展多项任务。
下面来说说并发的工作方式。我们可以将CPU执行的任务看作是线程,一个单核CPU在同一时间阶段内,开展多个线程处理,就体现出了并发。具体来说,操作系统会使用一种算法对线程进行调度,促使将一个CPU的资源可以合理地分配给多个线程(任务),其中每个线程都将分配一段,CPU为其执行的时间片,CPU会在多个线程之间不断切换,轮流的执行多个线程,也就是这个任务根据分配的时间片执行一会儿(10ms),在切换到另一个任务根据相应时间片再执行一会儿(10ms)。下图展示了两个线程并发执行的过程:
由于并发的方式促使线程切换速度很快,所以并发的执行通常对于用户的感觉而言,就像是多个任务并行一样,以致于产生了一种多个任务同一时间执行的假象。这一点听上去可能有点是是而非,你需要将并发的多个任务看作是在同一个时间阶段内执行的,而非是某个具体的时间点同时执行的。例如,两个任务都是在0到60秒这个阶段中完成的,但如果比较具体的执行时间点,一个任务某个执行时间点是08:30:12,另一个任务某个执行时间点则会是08:30:56。说白了就是,并发就是“一心二用”。这是因为单核CPU的计算机并没有能力在同一时间点运行多个线程。
并发的体现源于单核CPU资源合理的分配,促使多个线程在同一时间阶段内开展,并且有效避免了CPU被某个线程长期霸占的问题,提升了CPU资源利用率。但是,这种方式依然存在弊端。如果单核CPU处理的线程过多,CPU则会花费大量时间在这些线程之间进行切换,这会导致程序的性能下降。
4.并行
基于单核CPU的短板,并随着计算机硬件的发展,CPU迈入了多核时代,双核、四核、八核已屡见不鲜,甚至还有高达几十核的CPU。多核CPU不在局限于和单核CPU那种“一心两用”的工作方式,而是可以真正实现同一时间点执行多个线程(任务),达到“双管齐下”的效果。多核CPU的每个核心都可以独立地执行一个线程,并且多个核心之间不会相互干扰。因此,多核CPU在不同的核心上,在同一时间点,分别执行一个任务的这种方式,称之为并行。
例如,同样是执行两个任务,双核 CPU 的工作状态如下图所示:
通过上图我们可以看出,双核CPU可以在同一个时间各自执行一个任务,和单核CPU在两个任务之间不断切换相比,它的执行效率更高。需要注意的是,上图中CPU的核数与线程(任务)数刚好匹配,这是个理想状态,如果线程数大于了CPU的核数,那么计算机会按照什么样的方式执行呢?你可以思考一番,然后在下文中找到答案。
5.并发&并行
在实际的情况中,我们的计算机或智能手机,通常都会同时处理几百上千个线程(任务),并且对于目前的硬件条件而言,CPU具备的核数还无法普及或达到一个非常高的数值,所以线程(任务)数大于CPU核数是一个常态化的现象,对于这一点你可以打开任务管理器,通过查看CPU线程数就可以证实。
所以对于现实中存在的这种情况(核数低于线程数),计算机这个时候对于线程的处理,会同时存在并发和并行两种情况:所有的CPU核心都会并行工作,其中每个核心还会进行并发工作。例如一个双核 CPU 要执行四个任务,它的工作状态如图所示:
上图中每个核心并发执行了两个线程,两个核心并行就执行了四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,具体的分配还是要取决于操作系统的调度算法,以及每个线程的处理状态。
小结
并发的工作方式,会在单核CPU处理多个线程时出现,它代表了单核CPU交替执行不同线程的能力。并行的工作方式,只会在多核CPU处理的硬件条件下,并且线程数与核数相等情况下出现,它代表了多个核心同时执行多个任务的能力。在多核CPU中,并发核并行通常都会同时存在,两种工作方式的结合,会有效的提升计算机执行程序的效率。
6.概念类比
不要被枯燥、笼统的概念所吓跑。以上讲解的知识点都属于计算机的基本概念,初学者往往在读完这些概念后都会感觉比较模糊,这是正常的。其实计算机中大部分的概念,都可以通过生活中的场景进行类比。上文中讲解的概念也是如此。所以,接下来我将基于上文中的进程、线程、并发、并行的概念一起串一串,将它们融入到生活场景中进行类比。让这些概念可以形象化的展示在你面前。
进程
我们首先将应用程序看作是一家饭店。饭店通常在没有客人光临之前,都是一个相对静止的状态,这一点也和未启动的程序一样。当某个家庭来到饭店解决晚餐时,此时静态的饭店开始张灯结彩的欢迎客人了,此情此景可以看作一个程序开始启动了,而这个家庭来到饭店进行晚餐的活动,就类似于程序开启了一个进程。
线程
程序通过开启进程活动了起来,那么活动的进程中必定会开展相应的任务。这就等于你饭店在安置后客人就坐后,需要为客人烹制美味菜肴。我们来看看这个家庭点的菜肴:鱼香肉丝、湖北藕汤、剁椒鱼头、夫妻肺片。这些菜肴的制作通常对于一个标准化的饭店而言,都会在厨房中会划分不同的制作区域。例如,鱼香肉丝要在灶台区域、剁椒鱼头要在蒸柜区域。
当厨房要烹制某个菜品时,厨师就会根据菜肴的制作类型(炒、汤、蒸),到达指定的区域进行烹饪。对于这个现象,就像程序为实现不同的功能,会选择不同的代码路径执行一样。不同的菜肴要找到相应的区域进行烹制,并且为客人制作菜肴是饭店的主要任务,综上所述,我们可以将饭店为客人制作菜肴的任务,看作是进程中执行的线程。
并发
先别着急流口水,在点菜之后,我们将目光转向厨房。此时的厨房只有一名厨师在岗,所以他一个人将面临多道菜的制作,这名厨师并不打算一道菜一道菜的制作,因为他担心如果上菜太慢会导致:1.客人在吃前几道菜时就饱了,客人会放弃后面的菜;2.由于菜的间隔时间过长,最后一道菜上时第一道菜就已经凉了。所以他打算一个人先同时开展两道菜的制作,于是他在藕汤进行煨煮时,起锅烧油去锅里炒鱼香肉丝;当鱼香肉丝烧至入味时,去给藕汤进行调味,利用两个菜肴的空挡不断切换,最后当鱼香肉丝出锅时,藕汤也已经煨好了。以上这名厨师同一时间阶段内做多道菜的方式,就类似于与并发。
并行
此时老板来到厨房,看到你非常卖力的为客人准备菜肴,加上客人也有点着急,于是老板穿上了白大褂,戴起来高帽子,打算加入你的行列。此时的厨房就已经有两名厨师了,此时客人还只剩两道菜(剁椒鱼头、夫妻肺片)没有上。显然目前最佳的制作方式就是,两个厨师同时进行菜肴的制作,每个人负责一道菜肴。那么对于以上两名厨师同时进行菜肴的工作方式,就类似于并行。
并发&并行
在刚刚完成好上一桌客人的菜肴制作后,此时饭店又来了一桌客人,由于这桌客人聚餐的性质是公司聚餐,所以这桌客人点的菜肴有十几道菜。此时的厨房只有两名厨师,这就产生了一种情况:菜肴数大于厨师数,这也和线程数大于CPU核数同理。所以此时饭店的最佳工作方式就是,让两名厨师同时做菜,并且一个人负责多道菜的制作。对于这种情况,就类似CPU同时使用并发加并行两种方式开展任务。
7.多线程编程
多线程的实现可以从硬件或软件上体现。在硬件上,计算机基于单核CPU的并发或多核CPU并行的工作方式,并结合操作系统的线程调度程序,就可以实现多线程处理。在软件上,应用程序可以使用编程语言实现多线程的编码,从而实现在一个进程中创建多个线程,来完成一个程序中多项任务的同时处理。
目的
使用多线程的目的是为了同步完成多项任务。你可以试想下,你正要筹备一场年夜饭的食材。如果采购各式各样的食材全都是你一个人去完成,那么年夜饭的准备时长和开饭时间必定会延长。如果你安排你的家人进行协作,那么你的家人可以和你同时去购买不同的食材,这样一来会有助于节省你购买食材的时间。在这个例子中,你安排家人协作你购买食材,实际上就和编程中使用多线程的目的是一致的,编程中通过多线程会助于改善程序的总体响应性。
切换
多线程是把双刃剑,不是越多越好。每一个线程都需要分配独立的堆栈空间(耗费内存,如一个线程约占用1MB堆栈空间)。并且CPU对线程的切换需要保存很多中间状态、数据等,所以单个进程中的线程过多的话,性能反而会下降,CPU需要花费不少时间在各个线程之间来回切换,以致于耗费本该属于程序运行的时间。
共享
由于一个进程中所有的线程可以获取内存的共享数据,所以多个线程在同时访问某个数据时,会出现数据异常、不一致的情况。这可能会使程序发生非常奇怪、难以发现的bug,而且这些bug难以重现和调试。例如,线程A要写一块数据,同时线程B也要写这块数据。此时就需要采取一定的技术手段,让线程有先有后地去写,而不能同时去写,如果同时去写,可能写进去的数据就会出现互相覆盖等数据不一致的错误。
执行
从编码的角度来看,线程的执行仿佛是我们调用相应的函数来完成的,但实际上并非如此。我们调用线程执行的代码,并不会在程序执行这段代码时立即执行。准确的说,这段调用线程执行的代码,仅仅是通知操作系统尽快地执行这个线程。线程具体的执行,是由操作系统,根据线程调度程序的分配机制决定的。
结语
本文的基本概念只能作为多线程学习的一个开端,后续我将持续产出针对多线程应用的知识。想要将多线程技术更好的运用起来,可谓是,“路漫漫其修远兮”。多线程的技术熟练运用,不光是高级开发人员与中级开发人员之间的一道分水岭,它还是很多实际项目必须采用的一种技术方式。项目不单单只满足于功能而已,对于运行效率的提升,多线程技术的涉猎是不二法则。
戒骄戒躁,千万不要急于求成,不要以为多线程是一个很小的话题。多线程其实是一个很大的话题,请各位读者要稳扎稳打,一步一个脚印地把多线程学好,这会终身受益。