进程以及状态
之前说过线程,这回我们聊进程,他们有一定相似性,线程是CPU调度的基本单位,进程是资源分配的基本单位,也是线程的容器(进程包含线程)
比如我们运行了飞Q程序,打开任务管理器,就会看到飞Q在执行,还显示了CPU占用,内存占用,硬盘占用以及网络占用,所以,程序是固定不变的,进程会跟据操作系统进行系统资源分配
程序是静态的,但运行起来,代码加用到的资源成为进程。一个进程中包括多个线程(线程是轻量级的进程)
进程运行有几个状态,比如说并发的时候,cpu核小于任务数,比如图中的3任务,1个CPU,由于CPU是轮循的,执行1的一部分,1的进程就在运行状态,这时切换到2任务,2进入运行,1之前的准备资源完毕进入就绪状态。什么时候是进入等待状态呢,比如2遇到了time.sleep这时就会进入阻塞状态,同时开始3任务的运行状态。哪个任务执行完毕就进入死亡状态。如果下次2运行到就绪状态,下次轮询到就可以到运行状态。比如突然来了4任务,那这个任务就是新建启动进入就绪状态
进程基本使用
进程和线程代码又类似之处
1 导入模块import multiprocessing
2创建进程对象mulitprocessing.Process(target=xxx)
3实例对象启动start()
上图创建一个进程代码,我们使用多进程来实现,这里需要提出的是,进了程序执行的是主进程,而实例对象开启了一个子进程。注意这里创建子进程必须使用if __name__=='__main__':否则会提示报错
为了体现主进程和子进程在(几乎)同时运行,我们改成如上代码(的确是cpu轮询效果)
Process有如上图的语法,创建时,可以传入target,args,kwargs和线程一样,可以给线程起名字,指定组。还有图中的方法
进程的名称,pid】
进程名称获取(其实是获得当前进程)
之前的代码加上打印当前线程,就可以看到主进程和运行的子进程
如果我们对进程实例的时候加上了name参数,就会打印出指定的名字
获得当前进程有pid(process id)属性获得其编号,我们可以打印出主进程和子进程的编号
获得编号的另一种方法,调用os模块,os.getpid()可以获得当前进程id
获得父进程的id,仍是用os模块,os,getppid()获得父进程的id,上图子进程的父进程就是主进程
我们获得进程id有什么用,linux终端我们可以使用kill -9 进程编号 来结束进程
比如上图我们杀掉子进程kill -9 34968就会有如上结果,并没运行10次
我们杀掉主进程,会得到pycharm的如上提示,同样结束掉程序
进程参数传递,全局变量
子进程参数传递和子线程一致,通过args=元组,kwargs=字典 来传参
进程之间不能实现全局变量共享,如果声明,也只是将其copy一份(子进程会复制主进程资源)
守护主进程
守护主进程,主进程结束时,子进程也结束
我们写出如上代码,会发现主进程结束了,但是子进程还是没有结束。
我们想主进程结束子进程也跟着结束,需要使用实例属性赋值Process.daemon=True(和线程不一样,线程使用setDaemon方法),如下图
另一种方法,在主进程结束前使用实例Process.terminate()结束子进程
进程和线程的对比
功能上区别
2使用区别
比如上图内存执行着网易云音乐和天天静听的2个进程,每个进程都有下载和播放的线程,下载和播放线程之间共享着歌曲读写资源,但是进程间没有资源共享,互不影响,所以进程会更稳定。
线程不能单独运行,必须存在于进程中
对比图
选择原则
频繁创建优先线程,消耗资源小,切换速度快,大量计算使用线程,多机分布多进程,多核多线程,安全角度可能选择进程,速度角度选择线程,都满足,那就哪个熟悉拿手就行。但是cpython有GIL锁,当IO操作达到一定数量才会释放,造成多核cpu,多线程也是分时切换。
CPU密集型,进程优先,IO密集型,线程优先
消息队列一基本操作
由于进程间本身并不能共享资源,我们使用队列容器,来给多进程实现数据传递
队列如上图,一段发数据,另一端取数据,放入值put,取出值get
上图简单代码实现queue传入消息,实例时需要指定队列长度,上图放置完运行没问题。
当我们尝试再加入一个消息时,发现程序并没有结束,其实这时是进入了阻塞状态,因为最大长度为3,新消息进入,需要一个消息发送出
我们把超过队列长度的最后一个改成方法put_nowait,运行直接报错,这个方法会不阻塞等待,直接报错
我们可以将数据取出来打印,会发现先放进去的先取出来
如果我们想多取一个,程序就会进入阻塞状态,等待队列传入才能取出
同样我们可以使用get_nowait方法,这样当读不到消息就会报错,queue.Empty提示队列已空
消息队列一常见判断
上次结尾列举了就几个方法,这里我们调用下,Queue的方法empty和full判断是否空满,注意方法要加括号,返回Bool值。qsize方法返回队列的消息长度值
注意仔细看图中会发现有个坑,即放满了队列后,empty返回的结果也是True
看了官方文档的empty和full解释,的确也都加了not reliable不可信的解释,这是为什么呢,因为我们读取状态的时候,写入可能没有那么快,如果加入一定的延时如time.sleep可能就会使数据准确存放
Queue实现进程间的数据共享
进程通信思路:进程a写入put,进程b读取get
如上图,我们编写2个进程分别从队列中读写数据,会发现读的顺序快了,结果没写入就读完了
为了实现能读取成功,我们使用进程的jion方法,让写先完成,就出现如上结果,其实相当于单进程
进程池
我们为什么使用进程池,当我们创建进程数量不多时,可以用Process实例来生成,当进程数量大到千百个量级以上,我们就可以使用Pool方法创建生成多个进程的方法
Pool根据请求创建进程,初始设定进程上限,如果没到上限,有请求就继续创建,如果达到上限,就会等待池中进程有结束
Pool类实例核心方法
apply() 同步方式执行,池子里预先创建3个进程,比如上图3个文件拷贝,我们先启动进程1,用于拷贝文件1,结束后启动进程2,拷贝文件2,至启动进程3拷贝文件3,如果还有第四个任务,就会等进程池有进程结束,利用这个进程启动新任务(同一时间只有一个进程执行)
apply_async() 异步方式 如上图三个任务,三个进程同时启动,一起完成任务,如果还有第四个任务,就会等进程池有进程结束,利用这个进程启动新任务
代码应用,模拟同步拷贝文件
代码说明,使用进程池,设置进程池最大进程数,执行使用pool.apply(),apply有3个参数,func传入函数,args,kwargs之前进程实例也接触过,是用于参数传递。这里我们先不传参,在每个进程打印当前进程,会发现10个任务不是像我们想象中的1231231231这样的,其实是我修改了sleep时间,如果sleep时间长会形成类似顺序执行的结构,但是由于资源没释放资源准备等因素,进程池自身管理每个任务哪个进程来执行
一旦进程池创建,他们的pid就固定了,所以新任务来不是新创建线程,而是让空闲的进程执行新的任务
我们用过同步,就会摩拳擦掌来试试异步了,我们先把上面代码apply换成apply_async来试试,哎,怎么什么结果都没有就完事啦
我们怎么才能让异步实现呢,上面的问题是怎么回事,这里我们需要注意的是,异步进程池比同步进程池需要多2步操作,第一部是pool.close()这里关闭并不是不执行了,而是不再接收新的额外任务了,第二个是加上pool.join(),之前直接结束时因为使用异步,主进程就默认不会等待子进程结束,主进程结束,pool也就销毁了,于是池里的任务不会执行,我们使用join,这样主进程就会等待进程池任务都结束再结束。例中代码改慢sleep时间,会发现3个3个显示
进程池的Queue
标题即开始讲进程池间进程的通信
实例方法queue=multiprocessing.Manager().Queue(3)
比如我们实现个同步写入读取
我们实现异步写入读取,当然还是不要忘了,close,join
按照视频中代码,我们使用异步,并没有实现我们想要的功能,(各人感觉还不如充分利用queue读写的阻塞功能),这是因为pool分配资源,先准备好了读的功能,所以没读到(即使你write写在read前apply),这时我们怎么办呢
apply_async异步函数会返回一个对象,可以pycharm查看源码,使用对象的wait功能,就会实现类似join的功能,其实这样也变相变成了单进程,先写完再读
案例,多进程文件复制
视频教程是使用读写进程,这里直接使用shutil.copy(src,dst)来实现
这里需要注意的是,开始我尝试在进程函数内直接使用filelist,source_dir,tar_dir这几个参数,函数并没有报错,但是也没有执行下去,直接结束了,所以记得一定要把这些引用的看似全局的参数提供给子线程,
多进程版web服务器
具体代码不用详解了,这里用的是封装过的代码,需要注意的是多进程引用的函数是request_handler,我们传入了new_socket(accept返回),在函数里已经close掉,但是我们多线程创建完还是得close一遍,这就是因为子进程对new_socket拷贝了一份,引用计数相当为2,当函数内close,相当于引用计数变成1,但是还没有彻底关闭new_socket,还需要再close一回
可迭代对象
为什么学习:可迭代对象-》迭代器-》生成器-》协程
我们可以导入collections的Iterable类,用isinstance来判断是否可迭代
我们随便创建一个类对象,会发现是不可可迭代的
当我们给类添加iter魔法方法,这个类的实例就是可迭代的。
所以对象所属类有__iter__方法,即是可迭代对象
迭代器和使用方法
iter获得迭代器,多次使用next逐个获得迭代器元素
迭代器可以记录遍历的位置,配合next获得下个元素(如果迭代器到头,再next会引发StopIteration错误)
for循环本质,取得可迭代对象的迭代器,每次调用获得next迭代器的内容,遇到StopIteratioin异常停止
自定义迭代器需要类定义iter,next这2个魔法方法