并发编程之多进程2
multiprocessing模块
Python提供了multiprocessing模块。 该模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。
multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
需要再次强调的一点是:与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。
开启子进程的两种方式
注意:windows操作系统下 创建进程一定要在main内创建因为windows下创建进程类似于模块导入的方式,会从上往下依次执行代码,而linux中则是直接将代码完整的拷贝一份。
方式一:一般使用方式1创建并开启子进程
# 子进程的任务
def func(name):
print(f'{name} is start')
time.sleep(2)
print(f'{name} is done')
# 开启子进程的方式一:实例化Process类
if __name__ == '__main__':
# 创建一个子进程
p = Process(target=func, args=('xliu', ))
# 开启子进程
p.start()
print('主进程结束')
方式二:继承Process,重写父类的run方法
# 子进程的任务
def func(name):
print(f'{name} is start')
time.sleep(2)
print(f'{name} is done')
class MyProcess(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self): # start会自动调用run
func(self.name)
if __name__ == '__main__':
p = MyProcess('xliu')
p.start() # p.run()只有通过start开启的才是子进程,通过run开启的不是子进程
print('主进程结束')
注意:只有子进程对象调用start
方法才是开启的子进程。
# 测试:同时开启多个子进程
if __name__ == '__main__':
for i in range(1, 4):
p = Process(target=func, args=(f'p{i}', i))
p.start()
print('主进程结束')
# 输出:
主进程结束
p3 is start
p1 is start
p2 is start
p1 is done
p2 is done
p3 is done
# 同时开启了三个子进程,打印的结果,即执行的顺序是变化的
# 这是因为,子进程通过start开启,本质是应用程序向操作系统发起请求,让操作系统开子进程。
# 这是一个异步的操作,造作系统接到任务后,会根据整体情况做统筹安排,所以每个子进程开启的时间不按顺序,结束的时间也不按顺序。
join方法
join:主进程等待子进程结束
if __name__ == '__main__':
p = Process(target=func, args=('xliu', 1))
p.start()
p.join() # 主进程等待p这个子进程运行结束后再往下运行
print('主进程结束')
测试1:等待的是主进程,即卡在原地不动的是主进程,子进程开启后是正常运行的。
if __name__ == '__main__':
p_list = []
for i in range(1, 4):
p = Process(target=func, args=(f'p{i}', i))
p_list.append(p)
p.start() # 先开启所有子进程
for p in p_list:
p.join() # 再让主进程等待子进程
print('主进程结束')
# 输出:
p2 is start
p1 is start
p3 is start
p1 is done
p2 is done
p3 is done
主进程结束
# 当主进程等待子进程的时候,卡住的是主进程。此时所有的子进程已经交给操作系统开启了,子进程会正常运行的,等所有的子进程运行结束,主进程再运行。
# 每个子进程是在同时运行的,没有先后顺序之分。
# 所有主进程等待的时间应该是3s多,而不是1s+2s+3s
测试2:子进程开启就让主进程等待
if __name__ == '__main__':
for i in range(1, 4):
p = Process(target=func, args=(f'p{i}', i))
p.start()
p.join()
print('主进程结束')
# 输出:
p1 is start
p1 is done
p2 is start
p2 is done
p3 is start
p3 is done
主进程结束
# 此时就是主进程要挨个等待每一个子进程的结束再创建并开启新的子进程,这就变成了串行。
进程隔离内存空间
进程之间内存空间是隔离的,即进程见数据是隔离的,互不影响。
这是因为这是两个独立的名称空间。子进程有其自己的名称空间,只是修改其名称空间内全局变量的值,而不影响父进程自己名称空间全局变量的值。
money = 100
def task():
global money
money = 666
print('子', money) # 666
if __name__ == '__main__':
p = Process(target=task)
p.start()
p.join() # 主进程等待子进程p结束后再执行
print('主', money) # 100
进程对象的属性
进程对象是Process类的一个类对象(或者继承Process的类实例化的对象)。这个进程对象有很多数据属性和方法供我们直接使用。
from multiprocessing import Process, current_process
import os
# 进程对象的数据属性和函数属性
p.start() # 开启子进程
p.join() # 主进程等待p进程结束
p.run() # p进程具体执行的任务,可以重写这个方法
p.is_alive() # 判断子进程是否存活,返回布尔值
p.terminate() # 结束子进程, 异步调用操作系统,所以不会立马关闭
p.kill() # 结束子进程,同理
p.name # 获取子进程的名字或修改之,注意自定义类时name属性的命名冲突问题,先super再赋值
p.daemon # 判断子进程是否为守护进程,或者设置其为守护进程,p.daemon = True
# 补充:获取进程的pid
current_process().pid # 当前进程pid
os.getpid() # 当前进程pid
os.getppid() # 当前进程的父进程的pid
僵尸进程与孤儿进程
# 僵尸进程(有害)
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。详解如下
我们知道在unix/linux中,正常情况下子进程是通过父进程创建的,子进程在创建新的进程。
子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束;
如果子进程一结束就立刻回收其全部资源,那么在父进程内将无法获取子进程的状态信息。
因此,UNIX提供了一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息:
1、在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等)
2、直到父进程通过wait/waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait/waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用。但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
3、任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。
# 孤儿进程(无害)
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。
孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
总结:
- 僵尸进程是有害,且每一个进程都会经历僵尸进程的状态。
- 僵尸进程是为了让父进程获悉子进程的状态,方便子进程结束时回收其占用的内存资源。
- 孤儿进程是针对子进程而言的,子进程没有结束父进程就死了,此时子进程就成为孤儿进程。
- 孤儿继承会被系统的专门机制回收,因此孤儿进程是无害的,不会一直占用系统资源。
守护进程
主进程将一个子进程设置为守护进程,守护进程就会在主进程结束时随之结束不再运行。
-
守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
-
在开启子进程之前将其设置为守护进程
-
主进程代码运行结束,守护进程随即终止,不受别的子进程的影响。
from multiprocessing import Process
import time
def task():
print('i am coming')
time.sleep(2)
print('i am backing')
if __name__ == '__main__':
p = Process(target=task)
p.daemon = True # p.start()之前将子进程设置为守护进程,陪着主进程一块结束。
p.start()
time.sleep(0.1)
print('i am master')
互斥锁
进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的。
但是,共享带来的是竞争,竞争带来的结果就是错乱。控制的方式就是加锁处理,即每个进程依次使用资源。
加锁处理的结果就是:将并发变成串行,牺牲效率,保证数据的安全
from multiprocessing import Process, Lock
import json, time, random
# 查票
def search(i):
with open('data', 'r', encoding='utf8') as f:
dic = json.load(f)
print('用户%s查询余票:%s'%(i, dic.get('ticket_num')))
# 买票 1.先查 2.再买
def buy(i):
search(i)
time.sleep(random.randint(1,3))
if dic.get('ticket_num') > 0:
dic['ticket_num'] -= 1
with open('data','w',encoding='utf8') as f:
json.dump(dic,f)
print('用户%s买票成功'%i)
else:
print('用户%s买票失败'%i)
def run(i, mutex):
search(i)
mutex.acquire() # 抢锁, 给买票环节加锁处理
buy(i)
mutex.release() # 释放锁
if __name__ == '__main__':
# 在主进程中生成一把锁 让所有的子进程抢 谁先抢到谁先买票
mutex = Lock()
for i in range(1,11):
p = Process(target=run, args=(i, mutex)) # 通过参数传给每一子进程
p.start()
# 使用方式:# 在主进程中生成一把锁 让所有的子进程抢 谁先抢到谁先使用,抢不到的排队等候下次再抢
mutex = Lock()
mutex.acquire() # 抢锁
mutex.release() # 释放锁,还锁
# 补充:数据库的锁:行锁 表锁
总结:
- 锁不要轻易的使用,容易造成死锁现象(我们写代码一般不会用到,都是内部封装好的)
- 锁只在处理数据的部分加来保证数据安全(只在争抢数据的环节加锁处理即可)
进程通信
进程通信(IPC,Intent-Process Communication),即进程之间的通信,包括主进程-子进程;子进程-子进程
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是基于消息传递系统(Message passing system)。推荐使用队列,因为队列是采用管道+锁 实现的。
# 进程通信类型
- 共享存储器系统
- 管道通信系统
- 消息传递系统
- 客户端-服务端系统
# 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题;
# 而且在进程数目增多时,往往可以获得更好的可获展性。
队列
为什么使用队列的消息传递:
-
1 队列和管道都是将数据存放于内存中
-
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来
管道:subprocess, stdin stdout stderr 队列:管道+锁 实现的队列 队列:先进先出FIFO 堆栈:先进后出FILO
注意:
- 队列中不应该存放大数据
- 队列中的数据个数受限于内存的大小
通过multiprocessing模块导入Queue,使用队列,也可以直接导入 import queue
from multiprocessing import Queue
q = Queue(5) # 创建一个队列, 括号内可以传数字 表示生成的队列最大可以同时存放的数据量
q.put(123) # 往队列放数据,如果队列慢了,则原地等待,因为锁的原因
q.put_nowait(123) # 如果队列满了,不等待直接报错
q.get() # 从队列取值, 如果队列空,则原地等待
q.get_nowait() # 如果队列空的,不等在直接报错
q.full() # 判断队列是否满
q.empty() # 判断队列是否空
q.join() # 队列中所有的数据被取完再执行往下执行代码
进程间通信演示:
- 1.主进程跟子进程借助于队列通信
- 2.子进程跟子进程借助于队列通信
from multiprocessing import Queue, Process
def producer(q):
q.put('我是23号技师 很高兴为您服务')
def consumer(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
p1 = Process(target=producer,args=(q,))
p2 = Process(target=consumer,args=(q,))
p1.start()
p2.start()
q.put('start') # 主进程传数据
生产者消费者模型
生产者:制造产品;消费者:消费产品;媒介:产品存放的载体
生产者消费者模型 = 生产者(做包子的) + 消息队列(蒸笼) + 消费者(吃包子的)
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。
该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列.
消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
基于队列实现生产者消费者模型:JoinableQueue
from multiprocessing import Process, JoinableQueue
import time
import random
def producer(name,food, q):
for i in range(5):
data = '%s生产了%s%s'%(name,food,i)
# 模拟延迟
time.sleep(random.randint(1, 3))
print(data)
# 将数据放入 队列中
q.put(data)
def consumer(name, q):
while True:
food = q.get() # 没有数据就会卡住
q.task_done() # 告诉队列你已经从里面取出了一个数据并且处理完毕了
time.sleep(random.randint(1, 3))
print('%s吃了%s'%(name, food))
if __name__ == '__main__':
# q = Queue()
q = JoinableQueue() # 继承Queue, 比Queue多两个方法 join(), task_done()
p1 = Process(target=producer, args=('大厨egon', '包子', q))
p2 = Process(target=producer, args=('马叉虫tank', '泔水', q))
c1 = Process(target=consumer, args=('春哥', q))
c2 = Process(target=consumer, args=('新哥', q))
p1.start()
p2.start()
# 将消费者设置成守护进程, 由于 q.join()的存在,主进程结束时,生产的东西都被消费者消费了
c1.daemon = True
c2.daemon = True
c1.start()
c2.start()
p1.join() # 生产者先将产品生产出来
p2.join()
q.join() # 等待队列中所有的数据被取完再执行往下执行代码
# 只要q.join执行完毕 说明消费者已经处理完数据了 消费者就没有存在的必要了,所以设置为守护进程
补充:
# JoinableQueue 模块
每当你往该队列中存入数据的时候 内部会有一个计数器+1,
每当你调用task_done的时候 计数器-1
q.join() 当计数器为0的时候 才往后运行
总结:
- 生产者消费者模型的原理,当生产者生产结束后,主进程释放结束的信号。
- 生产者将数据放进队列后再统计队列数据长度。
- 每当消费者消耗一个数据,就q.task_done,消费者通知一声消费了一个数据。
- 在原地等待q.join(),当数据取完,则结束了。此时消费者没有存在的意义,应该设置成守护进程。