06 进程间通信IPC机制 队列
一、python进程间通信
IPC机制(Inter-Process Communication)
总结:
IPC机制 进程间通信 进程间数据是隔离的,一般情况下是无法进行数据交互的 但是可以通过一些手段实现通信 1.管道:(例如subprocess中的PIPE,只能用read取一次值再去取就报错了)效率太低,不适合频繁交互 2.队列(管道+锁实现):更强大,我们着重研究这个就好了
数据只有一份,取完就没了 无法重复获取用一份数据
1.1 队列 Queue模块
这个是进程模块中的队列Queue,是进程模块封装出来的高级队列,进程中还有可等待队列JoinableQueue
线程模块中没有队列,python有原生的队列模块(见下)
python原生的queue模块中包含:
queue.Queue(),先进先出
queue.LifoQueue(),先进后出
queue.PriorityQueue(),自定义优先级队列
from multiprocessing import Queue q = Queue() # 括号内支持传数字 限制的是队列的大小 # 队列: 先进先出 # 堆栈: 先进后出
q.put()放队列中放数据 q.get()从队列中取数据 # 上面两个方法在存放值和取值的时候都会出现阻塞的情况(队列满了,队列空了) q.full()判断队列是否存满(默认为几十亿) q.empty()判断队列是否为空 q.get_nowait()取值一旦没有值不等待直接报错 # 上面三个方法在多进程/多线程不适用
from multiprocessing import Queue q = Queue(3) # 括号内可以传参数 表示的是这个队列的最大存储数,不写基本上是无穷 # 往队列中添加数据 q.put(1) q.put(2) # print(q.full()) #False 判断队列是否满了 q.put(3) # print(q.full()) #True # q.put(4) # 当队列满了之后 再放入数据 不会报错 会原地等待 直到队列中有数据被取走(阻塞态) print(q.get()) print(q.get()) print(q.empty()) #False 判断队列中的数据是否取完 print(q.get()) print(q.empty()) # True # print(q.get_nowait()) # 取值 没有值不等待直接报错 # print(q.get()) # 当队列中的数据被取完之后 再次获取 程序会阻塞 直到有人往队列中放入值 """ full get_nowait empty 都不适用于多进程的情况 """
1.2 通过队列实现进程间通讯
Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递
验证通讯思路:
子进程放数据 主进程获取数据
两个子进程相互放、取数据
def producer(q): q.put('hello GF~') def consumer(q): print(q.get()) if __name__ == '__main__': q = Queue() p = Process(target=producer,args=(q,)) c = Process(target=consumer, args=(q,)) p.start() c.start() #hello GF~ 通讯成功
二、生产消费者模型 队列/可等待队列
生产者:生产/制造数据的
消费者:消费/处理数据的
例子:做包子的,买包子的两个问题
1.做包子远比买包子的多
2.没人买我不想做了,做包子的远比买包子的少
这就出现供需不平衡的问题
两者之间的通信介质:队列/管道
1.基于Queue
常规情况下包子买完后程序还停在那里不结束,在Queue队列下,在生产完之后有几个消费者就放入几个None,当两个人都取到None就停止,也行,但是太low,
from multiprocessing import Process,Queue #多进程组件,队列 import time,random #生产者方法 def producer(name,food,q): for i in range(4): time.sleep(random.randint(1,3)) #模拟获取数据时间 f = '%s生产的%s%s'%(name,food,i) print(f) q.put(f) #添加进队列 #消费者方法 def consumer(q,name): while True: food = q.get() #如果获取不到,会一直阻塞进程不会结束子进程 # 当队列中的数据是None的时候结束while循环 if food is None: print('%s获取到一个空'%name) break f = '%s消费了%s' % (name, food) print(f) time.sleep(random.randint(1,3)) # 模拟消耗数据时间 if __name__ == '__main__': q = Queue() # 创建队列 # 模拟生产者 生产数据 p = Process(target=producer, args=('p', '包子', q)) #创建进程 p.start() #启动进程 p1 = Process(target=producer, args=('p1', '烧饼', q)) p1.start() #模拟消费者消费数据 c = Process(target=consumer, args=(q, 'c')) c.start() c1 = Process(target=consumer, args=(q, 'c1')) c1.start() p.join()#阻塞主进程 直到p和p1 子进程结束后才执行q.put() 方法 p1.join()#阻塞主进程 直到p和p1 子进程结束后才执行q.put() 方法 #为了确保生产者生产完所有数据后, #最后一个是None,方便结束子进程中的while循环, #否则会一直等待队列中加入新数据。 q.put(None) q.put(None)
使用Queue
组件实现的缺点就是,实现了多少个消费者进程,就需要在最后往队列中添加多少个None,否则,p.get()
不到任务会阻塞子进程,因为while
循环,直到队列q
中有新的任务加进来,才会再次执行。而我们的生产者只能生产这么多东西,所以相当于程序卡死
2.基于JoinableQueue
进程模块中有,原生queue模块中没有
我们使用高级一点的能够被等待的队列JoinableQueue模块
JoinableQueue的实例p除了与Queue对象相同的方法之外,还具有以下方法:
q.task_done()
使用者使用此方法发出信号,表示q.get()返回的项目已经被处理。如果调用此方法的次数大于从队列中删除的项目数量,将引发ValueError异常。
q.join()
生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。
下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。
from multiprocessing import Process,Queue,JoinableQueue import random import time def producer(name,food,q): for i in range(10): data = '%s生产了%s%s'%(name,food,i) time.sleep(random.random()) q.put(data) print(data) def consumer(name,q): while True: data = q.get() if data == None:break print('%s吃了%s'%(name,data)) time.sleep(random.random()) q.task_done() # 告诉队列你已经从队列中取出了一个数据 并且处理完毕了 if __name__ == '__main__': q = JoinableQueue() p = Process(target=producer,args=('大厨egon','馒头',q)) p1 = Process(target=producer,args=('跟班tank','生蚝',q))
p.start()
p1.start()
c = Process(target=consumer,args=('兆龙',q)) c1 = Process(target=consumer,args=('吃货jerry',q))
c.daemon = True #将两个消费者作为守护进程,配合尾部q.join,只要取完队列主立马死,两个消费者也跟着死 c1.daemon = True
c.start() c1.start()
p.join() p1.join() q.join() # 等到队列中数据全部取出再结束主进程(和Queue中的q.empty类似但是empty只能用于单进程)
# q.put(None) # q.put(None) #在生产完之后放入两个None,当两个人都取到None就停止,也行,但是太low
三、python原生queue(堆栈、自定义优先级队列)
【问题】同一个进程下的多个线程本来就是数据共享,数据上锁就好了,为什么还要用队列?
- 因为锁操作的不好极容易产生死锁现象
- 队列是管道+锁 使用队列你就不需要自己手动操作锁的问题
1.正常队列 先进先出 q = queue.Queue() q.put('hahha') print(q.get())
print(q.get(block=False)) # 取不到不阻塞直接报错
2.先进后出 栈 q = queue.LifoQueue() q.put(1) q.put(2) q.put(3) print(q.get()) #3 3.自定义优先级 越小越优先 q = queue.PriorityQueue() q.put((10,'haha')) q.put((100,'hehehe')) q.put((0,'xxxx')) q.put((-10,'yyyy')) print(q.get()) #(-10, 'yyyy')
四、双端队列
deque实现了列表的双向插入和删除操作提高效率,适合用于队列和栈:
- append
- appendleft
- pop
- popleft
from collections import deque q = deque(['a','b','c']) q.append(1) q.appendleft(2) print(q) #deque([2, 'a', 'b', 'c', 1]) print(q.pop()) # 1 print(q.popleft()) # 2 #了解一下 #队列不应该支持任意位置插值,只能在首尾插值(不能插队是吧),但是双向队列有个傻子方法,插入,乱插 from collections import deque q = deque(['a','b','c']) q.insert(1,'哈哈哈') # 特殊点:双端队列可以根据索引在任意位置插值 print(q) #deque(['a', '哈哈哈', 'b', 'c'])