Python学习day37-并发编程(3)进程锁LockQueue队列生产者消费者模型JoinableQueue
Python学习day37-并发编程(3)
进程锁Lock
上文中我们提到抢票软件的原理,其实还可以做一个小优化,具体就要用到进程锁这个模块,进程锁可以起到和join相同的作用,但是有区别.
xxxxxxxxxx
* join是吧所有的子进程变成了串行
* 进程锁则是把锁住的代码变成了串行,相比之下进程锁更加方便,实用性泛用性也更强
示例代码如下:
xxxxxxxxxx
from multiprocessing import Process, Lock
import time
import json
import os
def search():
time.sleep(1) # 模拟网络io
with open('db.txt', 'rt', encoding='utf-8')as f:
res = json.load(f)
print(f'还剩{res["count"]}张票')
def get():
with open('db.txt', 'rt', encoding='utf-8')as f:
res = json.load(f)
# print(f'还剩{res["count"]}张票')
time.sleep(1) # 模拟网络io
if res['count'] > 0:
res['count'] -= 1
with open('db.txt', 'wt', encoding='utf-8')as f:
json.dump(res, f)
f.flush()
print(f'{os.getpid()}抢票成功')
time.sleep(1) # 模拟网络io
else:
print('票已经售空!~!~!~!~!~!~!~!')
def task(lock):
# search()
lock.acquire() # 进程锁锁住,同一时间只能拿有一个用于执行get函数,也就是购买函数
get()
lock.release() # 释放锁,下一个用户可以操作
if __name__ == '__main__':
lock = Lock() # 写在main里面,主进程里,让子进程拿到同一把锁
for i in range(5):
p = Process(target=task, args=(lock,))
p.start()
# p.join()# 因为加入了进程锁,所以join用来串行子进程的方式就可以弃用了
以上方法虽然相比之前有了一定的改进,但实际上还是很麻烦,并不适用于并发数量较多的情况,而且一方面效率比较低,另外一方面需要自己加锁处理.
所以其实我们还有一种更好更方便的方式来解决这个问题,就是队列Queue.
Queue队列
首先我们了解一下进程间通信的概念即可,简称是IPC,全称是Inter-Process Communication,就是字面意思,进程之间的通信,我们通常会采用Queue队列的方式.
队列的概念其实就是管道pipe加进程锁lock,其作用局势创建共享的进程队列,从而来实现进程之间的通信以及数据传递,实际上队列里面一般不会放入比较大的数据,大部分时候都是消息或者是一些非常小的数据.
Queue(maxsize)就是创建队列的方法,下面我们来看一下队列Queue的部分源码,笔者已经把那些复杂的实现原理删掉,我们只需要了解其有什么参数以及怎么使用就好:
xxxxxxxxxx
class Queue(object):
def __init__(self, maxsize=0, *, ctx):
pass
# 这里maxsize = 0就是队列的最大长度,如果没有手动赋值的话队列就不限长度.
def put(self, obj, block=True, timeout=None):
pass
# 这里,put方法,最常用的方法之一,将值放入队列中
# 其中的参数,obj即为要放入的值
# block是一种阻塞状态,默认为True,即阻塞,阻塞的意思就是如果队列长度已经满了,put不能放值进去,那么这个方法就会停止,不会继续向下执行.
# timeout即为时间延时,延时就是put方法在这些时间之内如果不能成功给队列放入值就会报错.要注意的是如果block为False的话延时是没有作用的,因为此时方法不具有阻塞作用,所以延时自然是无效的.
def get(self, block=True, timeout=None):
pass
# get,即是从队列中取值,其参数和用法和put完全一样,只是我们不用往里面放值而是按之前put进去的顺序往外取值.block和timeout的含义也和上面put一样
def qsize(self):
return self._maxsize - self._sem._semlock._get_value()
# 这个方法可以返回队列中的值的数量,但并非可靠值,因为队列中值会在不断的变化,使用时候要注意
def empty(self):
return not self._poll()
# 判断队列是否为空,返回值为布尔值True或者False
def full(self):
return self._sem._semlock._is_zero()
# 判断队列是否已经满了,返回值为布尔值True或者False
def get_nowait(self):
return self.get(False)
# 用法等同于get,但是没有block参数,相当于block = False
def put_nowait(self, obj):
return self.put(obj, False)
# 用法等同于put,但是没有block参数,相当于block = False
def close(self):
pass
# 字面意思,关闭当前队列,防止队列中加入更多的数据
生产者消费者模型
-
生产者:生产数据的任务
-
消费者:处理数据的任务
生产者<-->队列<-->消费者
生产者可以不停地生产,可以达到自己最大的生产效率 消费者可以不停的消费,也达到了自己最大的消费效率生产者消费者模型: 大大提高了生产者生产的效率和消费者消费的效率
所以在并发编程中,使用生产者和消费者模型可以解决绝大多数的并发问题.这个模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度.
要注意的一点是,队列Queue并不适合传输大型的数据或文件,通常应该传递消息之类,上文中也有提到.
以下为生产者和消费者的一个示例:
x
1from multiprocessing import Process, Queue
2
3
4def producer(q, name, food):
5'''生产者函数'''
6for i in range(3):
7print(f'{name}生产出来{food}{i}')
8res = f'{food}{i}'
9q.put(res)
10
11
12def consumer(q, name):
13'''消费者函数'''
14while True:
15res = q.get(timeout=2)
16if res is None:# 这里,当取到队列中的值是None时,跳出循环,即消费者停止消费
17print(f'{name}没东西了,木法吃了')
18break
19print(f'{name}吃了{res}')
20
21
22if __name__ == '__main__':
23q = Queue()
24p1 = Process(target=producer, args=(q, 'rocky', '包子'))
25p2 = Process(target=producer, args=(q, 'tank', '韭菜'))
26p3 = Process(target=producer, args=(q, 'nick', '蒜泥'))
27c1 = Process(target=consumer, args=(q, '111'))
28c2 = Process(target=consumer, args=(q, '222'))
29p1.start()
30p2.start()
31p3.start()
32c1.start()
33c2.start()
34p1.join()
35p2.join()
36p3.join()
37q.put(None)# 为什么这里要加入两个None呢,为了让两个消费者都能成功的跳出消费循环,不至于到最后报错队列为空.
38q.put(None)
39
其实这种处理方式有点傻,试想一下,如果这样处理生产者和消费者之间的关系,那么每多一个消费者我们就要多在队列里加入一个None,这样会非常的麻烦,而且代码也会显得非常的low.
所以我们可以借用JoinableQueue来完成这个事情.
JoinableQueue
JoinableQueue的用法和Queue非常的相似,但是有些许不同,让我们来看下JoinableQueue的源码如何:
x
class JoinableQueue(Queue):
def __init__(self, maxsize=0, *, ctx):
pass
# 我们可以看到,JoinableQueue的自身init里面和Queue基本上一模一样
def put(self, obj, block=True, timeout=None):
pass
# put的用法和Queue一样,参数也一样
def task_done(self):
pass
# 这个就是JoinableQueue和Queue最大的不同之一,我们可以看到,JoinableQueue里面并没有get方法,所以在调用get方法的时候是调用的JoinableQueue父类,也就是Queue的get方法.
# 而task_done的意义就是使用者调用此方法的时候会发出一个信号,表示q.get()返回的项目已经被处理掉了.
def join(self):
pass
# 这里的join同样为阻塞作用,不同的是在JoinableQueue里面,我们调用这个方法的时候会阻塞直到队列里所有的项目都会处理掉.处理掉的意思就是在所有的q.get()下面都要加上task_done的调用,这样join才会不再阻塞程序的运行.
# 上述概念我们可以理解为一个计数器
# 对这个计数器来说:
# put +1的操作
# task_done -1的操作
# q.task_done() 完成了一次任务,和get连用,减少计数器的计数
# q.join() 计数器为零才会不阻塞
那么,采用JoinableQueue改造过上面的生产者消费者模型的小实例为:
x
from multiprocessing import Process, JoinableQueue
import time, random
def producer(q, name, food):
'''生产者函数'''
for i in range(3):
time.sleep(2 * random.random())
print(f'{name}生产出来{food}{i}')
res = f'{food}{i}'
q.put(res)
def consumer(q, name):
'''消费者函数'''
while True:
res = q.get()
time.sleep(2 * random.random())
# if res is None:# 因为判定方式变了,我们没有向队列里加入None,所以这个判断无效了
# print(f'{name}没东西了,木法吃了')
# break
print(f'{name}吃了{res}')
q.task_done()# 这里是相当于队列的计数器减一
if __name__ == '__main__':
q = JoinableQueue()
p1 = Process(target=producer, args=(q, 'rocky', '包子'))
p2 = Process(target=producer, args=(q, 'tank', '韭菜'))
p3 = Process(target=producer, args=(q, 'nick', '蒜泥'))
c1 = Process(target=consumer, args=(q, '111'), daemon=True)
c2 = Process(target=consumer, args=(q, '222'), daemon=True)
# 这里在消费者的子进程生成时我们将其定义为守护进程,这样主进程在执行完毕后子进程就会随之结束,我们就不用再每添加一个消费者就要往队列里添加一个None了.
p1.start()
p2.start()
p3.start()
c1.start()
c2.start()
p1.join()
p2.join()
p3.join() # 生产者生产完毕
q.join() # 这里是主进程的最后一行代码
# 所以把消费者c1,c2做成守护进程的话,主进程最后一行一旦执行通过(q.join()通过也就是队列为空),就可以结束子进程,也就是消费者的两个子进程就会结束