自学python之路 第十章 进程和线程

自学Python之路

大数据实验室第五次学习记录打卡

第十章 进程和线程

10.1 简介

·无论是多核还是单核cpu,都可以执行多任务,而cpu执行代码都是按顺序执行的,那么单核cpu是如何执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,切换到任务3…这样反复执行下去。表面上看,每个任务都是交替执行的,但是由于cpu的执行速度实在太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核cpu上实现,但是,由于多任务数量远远多于cpu的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
·对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了个记事本进程。有些进程还不止同时干一件事,比如word,它可以同时进行打字、拼写检查等等,在一个进程内部,要同时干多件事,就需要同时运行多个子任务,我们把进程内的的这些子任务称为线程(Thread)。
·但是我们前面编写的所有python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?
有三种办法:1.多进程模式;2.多线程模式;3.多进程+多线程。

小结:线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多久时间。

10.2 多进程

·Unix/Linux操作系统提供了一个fork()系统调用,他非常特殊,调用一次,返回两次,因为操作系统自动把当前进程复制了一份(子进程),然后分别在父进程和子进程内返回。其中子进程永远返回0,而父进程返回子进程的ID。这样做的理由是一个父进程可以fork出很多子进程,所以父进程要记下每个子进程的ID,而子进程只需要调用getpid()就可以拿到父进程的ID。

· multiprocessing:由于python是跨平台的,自然也就有一个跨平台的多进程支持,multiprocessing模块就是跨平台版本的多进程模块。它提供了一个process类来代表一个进程对象:
在这里插入图片描述

·如果要启动大量子进程,可以用进程池的方式批量创建子进程:
在这里插入图片描述

·很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程之后还需要控制它的输入输出。subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入输出:
在这里插入图片描述
·如果子进程还需要输入,则可以通过communicate()方法输入:
在这里插入图片描述
·进程间通信: process之间肯定是需要通信的,python的multiprocessing模块包装了底层机制,提供了Queue、Pipes等多种方式来交换数据。以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:
在这里插入图片描述

10.3 多线程

·python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下只需要使用threading这个高级模块。
·启动一个线程就是把一个函数传入并创建Thead实例,然后调用start()开始执行:
在这里插入图片描述
Lock:
·多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响。而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容改乱了。
·当两个线程同时一取一存,就可能变量的值不对,要确保变量值正确,就需要创建一把锁,当某个线程开始执行时,就获得了锁,因此其他线程不能同时获得这把锁,只能等待,知道锁被上一个线程用完释放后,获得锁以后才能调用。从而避免修改的冲突。创建一个锁就是通过threading.Lock()来实现:
在这里插入图片描述

GIL:
·GIL是Python解释器执行代码时,对于任何线程执行时,必须先获得GIL锁,然后每执行100条字节码,解释器就会自动释放GIL锁,让别的线程能有机会执行。这个GIL全局锁实际上就是把所有线程的执行代码都给上了锁,所以,多线程在python中只能交替执行,即使100个线程跑在100核cpu上,也只能用到一个核。
·在python中,可以使用多线程,但是不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了python简单易用的特点了。不过,python虽然不能利用多线程实现多核任务,但是可以通过多进程实现多核任务,多个python进程各有各自独立的GIL锁,互不影响。

10.4 Threadlocal

·在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有自己能看见,不会影响其他线程,而全局变量的修改必须加锁。但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦,如下:
在这里插入图片描述
从上图可以看到每个函数一层一层调用都这么传参数特别麻烦,如果用全局变量也不行,因为每个线程处理不同的Student对象,不能共享。如果用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象如何?这种方法理论上是可行的,但是每个函数获取std的代码有点丑。所以ThreadLocal应运而生,如下:
在这里插入图片描述
·从上图可以看到,全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

10.5 进程 vs 线程

首先,要实现多任务,通常我们会设计Master-Worker模式,Master 负责分配任务,Worker负 责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。

如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。

多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进 程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。

多进程模式的缺点是创建进程的代价大,在Unix/Linux 系统下,用fork调用还行,在Windows 下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。

多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“ 该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。

在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定 性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。

线程切换:
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?

我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。

如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时, 这种方式称为单任务模型,或者批处理任务模型。

假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是- -样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。

但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。

操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄 存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换.过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。

所以,多任务一-旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。

计算密集型 vs IO密集型:

是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和I0密集型。

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

第二种任务的类型是I0密集型,涉及到网络、磁盘I0的任务都是I0密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待I0操作完成(因为I0的速度远远低于CPU和内存的速度)。对于I0密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是I0密集型任务,比如Web应用。

I0密集型任务执行期间,99%的时间都花在I0上,花在CPU.上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于I0密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

异步IO:

考虑到CPU和I0之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待I0操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。

现代操作系统对I0操作已经做了巨大的改进,最大的特点就是支持异步I0。如果充分利用操作系统提供的异步I0支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步I0的Web服务器,它在单核CPU.上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步I0编程模型来实现多任务是一个主要的趋势。

对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。

10.6 分布式进程

·在Thread和Process中,应当优选Process,因为它更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的对个cpu上。

·python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。

猜你喜欢

转载自blog.csdn.net/EMIvv/article/details/106314198