并发编程之多进程篇之四
主要知识点:互斥锁、队列和生产者消费者模型
一、互斥锁
1、进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,
而共享带来的是竞争,竞争带来的结果就是错乱。
如下实例:
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong
from multiprocessing import Process,Lock
import time
def task(name):
print('%s 1'%name)
time.sleep(1)
print('%s 2'%name)
time.sleep(1)
print('%s 3'%name)
if __name__ == '__main__':
for n in range(1,3):
p = Process(target=task,args=('进程%d'%n,))
p.start()
'''输出:
进程1 1
进程2 1
进程1 2
进程2 2
进程1 3
进程2 3
'''
那么,如何控制呢?答案就是加锁处理。
2、互斥锁的理解:
互斥锁的意思就是互相排斥,如果把多个进程比喻为多个人,互斥锁的工作原理就是多个人都要去争抢
同一个资源:比如卫生间,一个人抢到卫生间后上一把锁,其他人都要等着,等到这个完成任务后释放锁,其他
人才有可能有一个抢到,如此反复。
3、互斥锁的原理:
把并发改成串行,降低了效率,但保证了数据安全不错乱
互斥锁的实例如下:
# Lock 互斥锁保证了进程间的有序运行,但效率降低
def task(name,mutex):
mutex.acquire() # 加锁(添加互斥锁)
print('%s 1'%name)
time.sleep(1)
print('%s 2'%name)
time.sleep(1)
print('%s 3'%name)
mutex.release() # 释放互斥锁,再运行其他进程
if __name__ == '__main__':
mutex = Lock() # 父进程实例化一个互斥锁,子进程都会拷贝一份,需要传递给子进程
for i in range(1,3):
p = Process(target=task,args=('进程%d'%i,mutex))
p.start()
''' 输出:
进程1 1
进程1 2
进程1 3
进程2 1
进程2 2
进程2 3
'''
4、互斥锁的实际应用--模拟抢票程序
1、包括两个文件,分别是:模拟抢票.py 和 data.json文件。
模拟抢票.py
#-*- coding:utf-8 -*-
# write by congcong
import json
import time
from multiprocessing import Process,Lock
def search(name):
time.sleep(1)
dic = json.load(open('data.json','r',encoding='utf-8'))
print('<%s>查看剩余票数 【%d】'%(name,dic['count']))
def buy(name):
time.sleep(1)
dic = json.load(open('data.json','r',encoding='utf-8'))
if dic['count'] > 0:
dic['count'] -= 1
time.sleep(2)
json.dump(dic,open('data.json','w',encoding='utf-8'))
print('<%s> 购票成功!'%name)
def task(name,mute):
search(name) # 查询不需要加锁,可以并发执行
mute.acquire() # 加锁,获得锁的进程才能继续运行,串行执行
buy(name)
mute.release() # 解锁
if __name__ == '__main__':
mute = Lock()
for i in range(1,4):
p = Process(target=task,args=('路人%d'%i,mute))
p.start()
'''未加互斥锁时输出:
<路人2>查看剩余票数 【2】
<路人1>查看剩余票数 【2】
<路人3>查看剩余票数 【2】
<路人2> 购票成功!
<路人1> 购票成功!
<路人3> 购票成功!
'''
'''加互斥锁后输出:
<路人1>查看剩余票数 【2】
<路人2>查看剩余票数 【2】
<路人3>查看剩余票数 【2】
<路人1> 购票成功!
<路人2> 购票成功!
'''
data.json
{"count": 2}
2、互斥锁与join的区别(用程序代码来比较和解释)
同样两个文件,分别是:互斥锁与join的区别.py 和 data.json文件。
互斥锁与join的区别.py
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong
import time
import json
from multiprocessing import Process
def search(name):
time.sleep(1)
dic = json.load(open('data.json','r',encoding='utf-8'))
print('<%s> 剩余票数 [%d]'%(name,dic['count']))
def buy(name):
time.sleep(1)
dic = json.load(open('data.json','r',encoding='utf-8'))
if dic['count'] > 0:
dic['count'] -= 1
json.dump(dic,open('data.json','w',encoding='utf-8'))
time.sleep(1)
print('<%s> 购票成功!'%name)
else:
print('<%s> 购票失败!'%name)
def task(name):
search(name)
buy(name)
if __name__ == '__main__':
for i in range(1,4):
p = Process(target=task,args=('顾客%d'%i,))
p.start()
p.join()
'''
<顾客1> 剩余票数 [2]
<顾客1> 购票成功!
<顾客2> 剩余票数 [1]
<顾客2> 购票成功!
<顾客3> 剩余票数 [0]
<顾客3> 购票失败!
'''
# join 和 互斥锁 都可以将并发执行变为串行执行
'''
区别:join 对应修改的是全局的,而互斥锁则相对更加灵活,可以具体到全局的某一具体功能(共享)
'''
data.json
{"count":2}
注意二者区别:join 对应修改的是全局的,而互斥锁则相对更加灵活,可以具体到全局的某一具体功能(共享)。
二、队列
1、队列的理解
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,
这两种方式都是使用消息传递的。
2、队列的用法
2.1 创建队列的类(底层就是以管道和锁定的方式实现):
Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
2.2 参数介绍:
maxsize是队列中允许最大项数,省略则无大小限制。
但需要明确:
1、队列内存放的是消息而非大数据
2、队列占用的是内存空间,因而maxsize即便是无大小限制也受限于内存大小
2.3 主要方法
q.put 方法用以插入数据到队列中。
q.get 方法可以从队列读取并且删除一个元素。
2.4 队列实例
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong
from multiprocessing import Queue
q = Queue(3) # 括号内可指定最大存放数目,当未指定数目时,表示无限,但不建议这样,因为内存是有限的
q.put('hello world') # put()表示存放队列中(队列创建在内存中,可共享)
q.put({'a':1,'b':2})
q.put([1,2,3,4])
print(q.full()) # True ---> full()判断队列是否已满
#q.put(666) # 达到指定存放数目时,就会卡住,不再存放
print(q.get()) # hello world ---> 队列为先进先出的线性表
print(q.get()) # {'a': 1, 'b': 2}
print(q.get()) # [1, 2, 3, 4]
print(q.empty()) # True ---> empty() 判断队列是否为空
print(q.get()) # 当队列数据都完全取出后,再取不再运行
三、生产者消费者模型
1、 生产者消费者模型介绍
1.1 为什么要使用生产者消费者模型?
生产者指的是生产数据的任务,消费者指的是处理数据的任务,在并发编程中,如果生产者处理速度很快,
而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的
处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
1.2 什么是生产者和消费者模式?
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间
不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给
阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了
生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的。
2、生产者消费者模型实现
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong
import time
from multiprocessing import Process,Queue
# 解耦合;平衡了生产者和消费者的速度差
def producer(q):
for i in range(1,6):
good = '烤鸭%d'%i
time.sleep(0.5)
print('生产者生产了%s'%good)
q.put(good)
def consumer(q):
while True:
good = q.get()
if good is None:break
time.sleep(1)
print('消费者吃了%s'%good)
if __name__ == '__main__':
# 容器
q = Queue()
# 生产者们
p1 = Process(target=producer,args=(q,))
p2 = Process(target=producer,args=(q,))
p3 = Process(target=producer,args=(q,))
# 消费者们
c1 = Process(target=consumer,args=(q,))
c2 = Process(target=consumer,args=(q,))
c1.daemon = True # 守护进程,随主进程一同死去
c2.daemon = True
p1.start()
p2.start()
p3.start()
c1.start()
c2.start()
p1.join()
p2.join()
p3.join()
q.put(None) # 结束信号,位于生产者所有的产品最后,栈尾
q.put(None) # 结束信号的个数与消费者数量对应
print('来客人了!') # 主进程
'''
1、主进程需要等生产者p1、p2、p3进程结束后运行
2、p1、p2、p3是在消费者把所有数据取完后才会结束
3、等p1、p2、p3结束了,消费者也就没有存在的意义了,应该随主进程一同死掉,故消费者应设成守护进程
'''
四、利用 JoinableQueue模块改进生产者消费者模型
1、JoinableQueue(maxsize)的理解
这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
2、参数介绍
maxsize是队列中允许最大项数,省略则无大小限制。
3、方法介绍
JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:
q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
4、改进后的程序
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
# write by congcong
import time
from multiprocessing import Process,JoinableQueue
def producer(q):
for i in range(1,5):
good = '烤红薯%s'%i
time.sleep(1)
print('生产者生产了烤红薯%s'%good)
q.put(good)
q.join() # 等待队列进程执行完,即队列内数据被取完
def consumer(q):
while True:
good = q.get()
if good is None:break
time.sleep(1)
print('消费者吃了%s'%good)
q.task_done() # 给生产者发送结束信号,即队列已经有一个数据被取走了
if __name__ == '__main__':
# 容器
q = JoinableQueue()
# 生产者
p1 = Process(target=producer,args=(q,))
p2 = Process(target=producer,args=(q,))
p3 = Process(target=producer,args=(q,))
# 消费者
c1 = Process(target=consumer,args=(q,))
c2 = Process(target=consumer,args=(q,))
c1.daemon = True # 将消费者进程设为守护进程,以便消费者进程随主进程结束而结束
c2.daemon = True
# 发送信号
p1.start()
p2.start()
p3.start()
c1.start()
c2.start()
p1.join()
p2.join()
p3.join()
print('主进程')
五、生产者消费者模型总结
5.1 程序中有两类角色
一类负责生产数据(生产者)
一类负责处理数据(消费者)
5.2 引入生产者消费者模型为了解决的问题是
平衡生产者与消费者之间的速度差
程序解开耦合
5.3 如何实现生产者消费者模型
生产者<--->队列<--->消费者