08_02、开启多进程
运行中的程序就是一个进程。
所有的进程都是通过它的父进程来创建的。
因此,运行起来的python程序也是一个进程,那么我们也可以在程序中再创建进程。
多个进程可以实现并发效果,也就是说,当我们的程序中存在多个进程的时候,在某些时候,就会让程序的执行速度变快。
一、multiprocess模块
仔细说来,multiprocess不是一个模块而是python中一个操作、管理进程的包。
之所以叫multi是取自multiple的多功能的意思,在这个包中几乎包含了和进程有关的所有子模块。
由于提供的子模块非常多,为了方便大家归类记忆,可以将这部分大致分为四个部分:
创建进程部分
进程同步部分
进程池部分
进程之间数据共享。
二、multiprocess.process模块
process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。
from multiprocessing import Process
三、process模块介绍
1、Process类的参数介绍
group
target name args kwargs
强调:
- 需要使用关键字的方式来指定参数
- args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
参数介绍:
- group参数未使用,值始终为None
- target表示调用对象,即子进程要执行的任务
- args表示调用对象的位置参数元组,
args=(1,2,'egon',)
- kwargs表示调用对象的字典,
kwargs={'name':'egon','age':18}
- name为子进程的名称
2、Process类的方法介绍
p.daemon
:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()
之前设置p.name
:进程的名称p.pid
:进程的pidp.exitcode
:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)p.authkey
:进程的身份验证键,默认是由os.urandom()
随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
3、Process类的属性介绍
p.start()
:启动进程,并调用该子进程中的p.run()p.run()
:进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法p.terminate()
:强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁p.is_alive()
:如果p仍然运行,返回Truep.join([timeout])
:主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
# 从multiprocessing包导入Process(进程模块) from multiprocessing import Process import time def write_file(): time.sleep(2) print('子进程:来自子函数') # 注意Windows系统中,开启进程需要写入__main__ if __name__ == '__main__': # 1.实例化对象得到p进程 p = Process(target=write_file, name='wf') # target参数:表示调用对象,即子进程要执行的任务 # name参数:表示子进程的名称 # 2.开启进程 p.start() # 通知操作系统启动进程,并调用子进程中的p.run() p.terminate() # 通知操作系统终止进程 # 但是由于通知系统也需要时间,所以进程并不会立即终止 # time.sleep(1) # 此处停顿一秒,可以看到is_alive返回值为False # 此处不停顿,返回值为True,因为终止命令还未传送给操作系统 print(p.is_alive()) # p.is_alive()判断进程是否存活,返回值为布尔 p.join() # 让主进程等待子进程执行完毕再继续执行 print('end') # 不用join方法,正常执行顺序:先执行主进程‘end’
四、查看进程编号
方法一:os模块
子进程PID:os.getpid()
父进程PID:os.getppid()
方法二:Process模块
主进程PID:p.pid
五、开启多个进程
# 多进程就是实例化多个对象并运行 # 此处通过循环实例化实现多进程 from multiprocessing import Process import time def write_file(): with open('a.txt', 'a', encoding='utf-8') as f: f.write('正大光明\n') print('子进程') time.sleep(1) if __name__ == '__main__': ctime = time.time() ll = [] # 实例化5个对象并放入空列表,表示开了5个进程 for i in range(5): p = Process(target=write_file, name='wf', ) p.start() # 通知操作系统开进程 ll.append(p) # 追加进列表 for j in ll: # 遍历全部进程 j.join() # 让主进程等待子进程全部运行完毕 print('time:', time.time() - ctime) # 最后运行主进程的时间代码
六、进程锁
通过导入Lock模块,实现进程锁的目的
lock.acquire() # 进程锁锁定
lock.release() # 释放进程锁
引入进程锁是为了保证数据的安全性、准确性:
多个进程修改同一块数据时,保证了同一时间只能有一个任务可以进行修改,即串行的修改,虽然速度慢了,但牺牲了速度却保证了数据安全
# 进程锁保证了各个进程的先进先出 # 注意:各个进程之间的执行速度不是按1,2,3,4顺序执行,而是看进程的速度 from multiprocessing import Process, Lock import time def write_file(i, lock): # 上锁 lock.acquire() print('第%s个进程进来了' % i) time.sleep(1) print('第%s个进程走了' % i) # 释放锁 lock.release() if __name__ == '__main__': lock = Lock() for i in range(1, 5): p = Process(target=write_file, args=(i, lock)) p.start()
七、进程间的数据隔离
1、进程间的数据隔离问题
进程之间的数据是隔离的,不能相互使用
# 验证进程间的数据隔离
from multiprocessing import Process n = 100 def write_file(): global n # 通过global验证 # 不在进程中,函数内n为200,函数外n也为200 # 在进程中,子进程n为200,函数外n为100 # 说明开启进程时,global未发生作用 # 可以验证,各个进程之间的数据相互隔离 n = 200 print('子进程中的n:', n) if __name__ == '__main__': p = Process(target=write_file, ) p.start() print('主进程中的n: ', n)
2、Queue队列
1、队列概念
队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。
2、Queue队列
Queue队列是一种数据结构.它有两个基本操作:在队列尾部加人一个元素,和从队列头部移除一个元素,就是说,队列以一种先进先出的方式管理数据。
如果你试图向一个 已经满了的阻塞队列中添加一个元素或者是从一个空的阻塞队列中移除一个元索,将导致线程阻塞.
3、Queue队列方法
Queue([maxsize])
:创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。
Queue的实例q具有以下方法:
q.get( [ block [ ,timeout ] ] )
:返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True.
如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。
q.get_nowait()
:同q.get(False)
方法。
q.put(item [, block [,timeout ] ] )
:将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。
q.qsize()
:返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。
q.empty()
:如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。
q.full()
:如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()
方法)。
4、其他补充:
q.close()
:关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()
操作上,关闭生产者中的队列不会导致get()
方法返回错误。
q.cancel_join_thread()
:不会再进程退出时自动连接后台线程。这可以防止join_thread()
方法阻塞。
q.join_thread()
:连接队列的后台线程。此方法用于在调用q.close()
方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()
方法可以禁止这种行为。
5、补充数据结构
队列:先进先出
栈:先进后出
链表(单链表、双链表、循环列表)
栈、队列、树(二叉树、平衡树、红黑树、b+树、b-树)、图
# Queue队列基础使用 from multiprocessing import Queue if __name__ == '__main__': q = Queue(3) # 入队列 q.put('hello world1') q.put('hello world2') q.put('hello world3') # q.put('hello world4') # 队列内只有三个位置,如果队列内没有空位(未被取走),依然给队列传数据,会造成阻塞 # 只有队列内腾出位置(数据被取走),新数据才能进入队列 # put(self, obj, block=True, timeout=None) 参数 # block默认值为True,如果为False,且队列内放不进去,会立马报错,不会阻塞 # timeout表示执行时间,时间段内阻塞(block=True默认值),时间段后报错 # q.put('hello world4', block=False) # 报错 # q.put('hello world4', timeout=3) # 3s内被取走(进入队列),不报错;3s后未被取走报错 # q.put_nowait('aaaaa') # 等同于block,不等待直接报错 print(q.get()) # 取出hello world1 print(q.get()) # 取出hello world2 print(q.get()) # 取出hello world3 # print(q.get()) # 队列内已没有数据,再次取会阻塞,等待新的数据传入队列 # q.get()同q.put() # get(self, block=True, timeout=None) # block=True, timeout=None # print(q.get()) # 再取阻塞 # q.get_nowait() # 不等待报错 print(q.qsize()) # 查看队列内是否有几个等候数 print(q.full()) # 查看队列内排队是否满员
3、解决进程间的数据隔离问题
# 通过Queue实现进程间的数据传输 from multiprocessing import Process, Queue def write_file(q): q.put('我是子进程') if __name__ == '__main__': q = Queue(3) # 队列内的排队数为3 p = Process(target=write_file, args=(q,)) p.start() # 通过q.get(),主进程可以渠道子进程的数据 print('主进程: ', q.get())
八、生产者消费者模型
# 此版本模型:多生产者,少消费者(3个生产者,2个消费者) from multiprocessing import Process, Queue import os """ 生产者和消费者模式能够解决绝大多数并发问题 生产者和消费者彼此之间不直接通讯 消费者不找生产者要数据 """ # 1. 生产者 def producer(q, food): # 让生产者生产10份食物 for i in range(1, 11): data = '%s生产的第%s份%s' % (os.getpid(), i, food) print(data) q.put(data) # 2. 消费者 def consumer(q): while True: data = q.get() if data is None: break print('%s消费了%s' % (os.getpid(), data)) if __name__ == '__main__': q = Queue(30) # Queue队列内有30个位置 # 3个生产者 p = Process(target=producer, args=(q, '烧饼')) p1 = Process(target=producer, args=(q, '小笼包')) p2 = Process(target=producer, args=(q, '胡辣汤')) p.start() p1.start() p2.start() # 2个消费者 p4 = Process(target=consumer, args=(q,)) p5 = Process(target=consumer, args=(q,)) p4.start() p5.start() # p.join保证主进程等待子进程执行完毕后再执行 p.join() p1.join() p2.join() # 两个q.put(None)是保证两个消费者在队列内取不到数据是不会阻塞 # 而且必须放在p.join()下面,保证子进程执行完毕后再执行主进程的q.put(None),以便结束循环 q.put(None) q.put(None)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通