这里写目录标题
一、多进程
1、进程的定义:
顾名思义,就是进行中的程序。进程是python中最小的资源分配单元,进程之间的数据,资源是不共享的、是隔离的;每启动一个进程,都要独立分配资源和拷贝访问的数据;进程是重量级别的,在进程中,需要处理的问题包括进程间通信,临界区管理和进程调度,所以进程的启动和销毁的代价是比较大的。
- 1、windows任务管理器中的每个任务都是一个进程
- 2、进程可以直接占用CPU、内存、磁盘、网络、GPU
- 3、打开一个浏览器就是,就是启动了一个浏览器进程;打开一个记事本,就是启动了一个记事本进程。
2、单核多任务CPU执行原理
10年前:单核多任务cpu运行:
整个cpu分割成多个cpu时间片段,每个时间片段执行一个任务,当执行任务1时,任务2和任务3处于等待状态,因为时间片段很短毫秒级别的,所以当把时间拉长为1s时,这些任务都执行了,给人的感官是并行执行的。
3、进程的优点和缺点
优点:
1、可以使用计算机多核,进行任务的并发执行,提高执行效率,运行不受其他进程影响,创建方便
2、空间独立,数据安全
缺点:
1、进程的创建和删除消耗的系统资源较多
2、全局变量在多个进程中不能共享
3、在子进程中修改全局变量对父进程中的全局变量没有影响。因为父进程在创建子进程时对全局变量做了一个备份,父进程中的全局变量与子进程的全局变量完全是不同的两个变量。全局变量在多个进程中不能共享。
4、创建进程1
创建进程用到Process类。
p = Process(target=sub_process_run, args=('子进程1',), name="子进程1")。
- target:运行的函数。
- args:传入到子进程中的参数。
- name:子进程名字。
- 主进程:读取01创建进程.py文件,边解释边运行;当运行到第17行的时候,创建一个子进程,在已有的主进程的之上又创建了一个子进程。
- 子进程的名字为子进程1。
- 如果启动了子进程(p.start()),那么就调用target函数,如果函数有参数,那么就通过args中的参数进行传递。
- p.start():启动子进程。
- p.join():让主进程一直等待,直到p这个子进程结束。
import os
import time
from multiprocessing import Process
def sub_process_run(name):
time.sleep(5)
print(f'子进程的名字:{name}')
print(f'子进程ID是:{os.getpid()}')
if __name__ == '__main__':
print(f'主进程ID是:{os.getpid()}')
# 创建进程,target:运行的函数
# args:传入到子进程中的参数
# name:子进程名字
p = Process(target=sub_process_run, args=('子进程1',), name="子进程1")
# 主进程:读取01创建进程py文件,边解释边运行;当运行到第17行的时候,创建一个子进程,在已有的主进程的之上又创建了一个子进程
# 子进程的名字为子进程1
# 如果启动了子进程(p.join()),那么就调用target函数,如果函数有参数,那么就通过args中的参数进行传递
# 启动子进程
p.start()
p.join() # 让主进程一直等待,p这个子进程结束
print('主进程结束')
5、创建进程2
自定义一个进程类,需要继承Process类。
进程启动之后自动调用的函数;只有run()这块代码才是子进程执行的代码。
代码中3个进程是同时执行任务的——》多进程并行执行。
p = MyProcess(name=f"进程{i}")
- p.start():启动子进程。
- p.join():主进程等待3个子进程全部执行结束后,开始执行主进程。
- join():是一个阻塞函数
- 为什么要用列表? 10个子进程可以并行执行
- 多线程是并发。
- 多进程是并行。
import os
import time
from multiprocessing import Process
class MyProcess(Process):
"""
这是定义一个自己的进程类
"""
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
"""进程启动之后自动调用的函数
只有这块代码才是子进程执行的代码
"""
time.sleep(5)
print(f'子进程的名字:{self.name}')
print(f'子进程ID是:{os.getpid()}')
print(f'子进程ID{os.getpid()}结束')
if __name__ == '__main__':
print(f'主进程ID是:{os.getpid()}')
process_list=[]
# todo 3个进程是同时执行任务的——》多进程并行执行
for i in range(3):
# 创建子进程
p = MyProcess(name=f"进程{i}")
# 启动子进程
p.start()
process_list.append(p)
for p in process_list:
p.join() # 主进程等待3个子进程全部执行结束后,开始执行主进程
print('主进程结束')
'''
多线程是并发
多进程是并行
'''
6、进程池
大家思考一个问题:在一台计算机中进程可以无限制的创建吗?
6.1、进程池的作用
进程池的作用:当进程数过多,用于限制进程数。pool可以提供和指定数量的进程,供用户调用。当有新的请求提交到pool中时。如果池还没有满,那么就会创建一个新的进程来执行该请求;但如果池中的进程数已经达到规定的最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程。
6.2、原理图
绿色:子进程没有工作,处于空闲状态(子进程没有调用任务函数)
粉色:子进程处于工作状态(子进程调用了任务函数)
6.3、使用进程池的优点
1.提高效率,节省开辟进程和开辟内存空间的时间及销毁进程的时间
2.节省内存空间
进程池使用Pool类
pl = Pool(5)
:创建进程池,最多支持5个进程同时执行
- Pool(5):创建多个进程,表示可以同时执行的进程数量为5个。默认大小是CPU的核心数果。
join():进程池对象调用join,会等待进程池中所有的子进程结束完毕再去结束父进程。
close():如果我们用的是进程池,在调用join()之前必须要先调用close(),并且在close()之后不能再继续往进程池请求
pl.apply_async(func=sub_process_run, args=(f'进程{i}',))
pl.close():进程池关闭
import os
import time
from multiprocessing import Process, Pool
def sub_process_run(name):
"""进程启动之后自动调用的函数
只有这块代码才是子进程执行的代码
"""
print(f'子进程的名字:{name}')
print(f'子进程ID是:{os.getpid()}')
print(f'子进程ID{os.getpid()}结束')
time.sleep(5)
if __name__ == '__main__':
print(f'主进程ID是:{os.getpid()}')
pl = Pool(5) # 创建进程池,最多支持5个进程同时执行
for i in range(10): #现在有10个请求进到进程池中
pl.apply_async(func=sub_process_run, args=(f'进程{i}',))
pl.close() # 进程池关闭
pl.join()
print('主进程结束')
注意:
因为我们Pool(4)指定了同时最多只能执行4个进程(Pool进程池默认大小是CPu的核心数),但是我们多放入了6个进程进入我们的进程池,所以程序一开始就会只开启4个进程。
而且子进程执行是没有顺序的,先执行哪个子进程操作系统说了算的。而且进程的创建和销毁也是非常消耗资源的,所以如果进行一些本来就不需要多少耗时的任务你会发现多进程甚至比单进程还要慢。
7、进程间的通信(Queue)
大家思考一下:在多进程中可以使用global+全局变量来共享数据吗?
现在设想你需要两个进程,
一个进程(接收进程)产生数据(比如从网站上爬虫,或者从websocket接收数据等),
另一个进程(转发进程)对产生的数据进行处理并转发(比如计算并处理之后上传数据库,或者发送给websocket等)。这是一个非常常见的应用场景,
直接硬写global+变量是不行的。
使用Queue类实现进程之间的通信
初始化Q对象:q=Queue()
将数据加入到队列中:q.put(100)
从队列中拿出数据:a = q.get()
进程间数据通信之Queue示意图:
mq.put(i) 子进程1发送数据到队列Queue中,子进程2和子进程3从队列中获取数据
Queue可以称为通信的中间件
7.1、需求1:采用多进程将100加到110
import os
import time
from multiprocessing import Process, Pool,Queue
'''
进程池中不能用Queue
'''
def sub_process_run(name,q):
"""进程启动之后自动调用的函数
只有这块代码才是子进程执行的代码
"""
print(f'子进程的名字:{name}')
print(f'子进程ID是:{os.getpid()}')
print(f'子进程ID{os.getpid()}结束')
time.sleep(5)
#从队列中拿出数据
a = q.get()
a += 1
q.put(a)
print(f'子进程{os.getpid()}结束,a的值为{a}')
if __name__ == '__main__':
q=Queue()
#将数据加入到队列中
q.put(100)
print(f'主进程ID是:{os.getpid()}')
pl=[]
# todo 3个进程是同时执行任务的——》多进程并行执行
for i in range(10):
# 创建子进程
p = Process(target=sub_process_run,args=(f'进程{i}',q))
# 启动子进程
p.start()
pl.append(p)
for p in pl:
p.join() # 主进程等待3个子进程全部执行结束后,开始执行主进程
print('主进程结束')
"""
阻塞函数有哪些?join、recv
"""
7.2、需求2
我们有两个进程,一个进程负责写(write)一个进程负责读(read)。当写的进程写完某部分以后要把数据交给读的进程进行使用,这时候我们就需要使用到了multiprocessing模块的Queue (队列):write(将写完的数据交给队列,再由队列交给read()
mq.put(i) writer进程负责把数据写入Queue
mq.get(True) 负责从Queue中读取数据,get函数是一个阻塞的函数,当队列中没有数据时,会一直阻塞在这里。
pr.terminate() 强制杀死pr进程
8、进程间的通信(Pipe)
Pipe直译过来的意思是"管"或"管道",该种实现多进程编程的方式,和实际生活中的管〈管道)是非常类似的。通常情况下,管道有2个口,而Pipe 也常用来实现2个进程之间的通信,这2个进程分别位于管道的两端,—端用来发送数据,另一端用来接收数据。
1、导入:from multiprocessing import Pipe
2、进程间数据通信之Pipe示意图:
1、p1,p2=Pipe():Pipe创建之后得到管道的两端,必须这样写,不能写成(p1=Pipe(),p2=Pipe())
2、self.pipe.send(i) :write进程负责把数据通过管道发送给另一个进程
3、value=self.pipe.recv():当管道中没有数据,该行代码一直阻塞
4、recv函数是阻塞函数
8.1、Queue和Pipe的区别:
Queue:实现多个进程之间通信的
Pipe:实现1对1,单个进程之间的通信
二、计算(CPU)密集型和IO密集型
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、浮点运算、对视频进行高清解码(每一帧是一张图片,一秒钟有25帧,也就是说视频的每秒有25张图片,一张图片又有很多的像素)等等,全靠CPU的运算能力。
- 计算密集型应该充分使用CPU资源,那就需要使用多进程
这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
第二种任务的类型是IO(Input、Output)密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。