十.进程同步锁(互斥锁/排它锁)
上面我们实现了进程的并发, 进程之间的数据是不共享的, 但是他们可以共享同一个文件(硬盘空间), 或者是同一个打印空间, 然而在共享的同时也带来了问题 : 进程的运行不是同时进行的, 它们没有先后顺序, 一旦开启也不受我们的限制, 当多个进程使用同一份数据资源时, 就会引发数据安全或者数据混乱问题
1.什么是互斥锁
我们打个简单的比方, 公司里的一台打印机, 每个人都可以使用, 但同事只能有一个人在使用, 不然就会造成打印错乱; 又比如合租房的卫生间, 合住的同伴都可以使用卫生间, 但每次只能一个人进去, 进去之后门就锁上了(相当于加锁 Lock( ).acquire
( )), 出来之后开门, 其他人又可以使用卫生间了(相当于解锁Lock( ).release( )
)
🍓余票文件 "aaa.json"
{"count": 1} # 剩一张票
🍓模拟多个人抢票
# coding:utf-8
from multiprocessing import Process
import os,time,json
def check(): # 先查票
time.sleep(1) # 模拟网络延迟
with open("aaa.json")as f:
dic = json.load(f)
print(f"剩余票数 : {dic['count']}")
def get(): # 查完之后开始抢
time.sleep(1) # 模拟网络延迟
with open("aaa.json")as f:
dic = json.load(f)
if dic["count"] >0:
dic["count"] -= 1
time.sleep(1) # 模拟网络延迟
with open("aaa.json","w")as f2: # 抢完之后修改数据并提交到服务端
json.dump(dic,f2)
print(f"用户 : {os.getpid()} 抢票成功")
else:
print(f"用户 : {os.getpid()} 抢票失败")
def run():
check()
time.sleep(1) # 模拟网络延迟
get()
if __name__ == "__main__":
for i in range(4):
p = Process(target=run)
p.start()
'''输出
剩余票数 : 1
剩余票数 : 1
剩余票数 : 1
剩余票数 : 1
用户 : 13116 抢票成功
用户 : 2364 抢票成功
用户 : 1796 抢票成功
用户 : 6228 抢票成功
'''
打印的结果发现只有一张票, 但是四个人都抢成功了, 这就非常不合理,造成了数据混乱
# coding:utf-8
from multiprocessing import Process,Lock
import os,time,json
def check(): # 先查票
time.sleep(1) # 模拟网络延迟
with open("aaa.json")as f:
dic = json.load(f)
print(f"剩余票数 : {dic['count']}")
def get(): # 查完之后开始抢
time.sleep(1) # 模拟网络延迟
with open("aaa.json")as f:
dic = json.load(f)
if dic["count"] >0:
dic["count"] -= 1
time.sleep(1) # 模拟网络延迟
with open("aaa.json","w")as f2: # 抢完之后修改数据并提交到服务端
json.dump(dic,f2)
print(f"用户 : {os.getpid()} 抢票成功")
else:
print(f"用户 : {os.getpid()} 抢票失败")
def run(lock):
check()
time.sleep(1) # 模拟网络延迟
lock.acquire() # 在抢票环节加锁
get()
lock.release() # 抢完后解锁
if __name__ == "__main__":
lock = Lock()
for i in range(4):
p = Process(target=run,args=(lock,))
p.start()
'''输出
剩余票数 : 1
剩余票数 : 1
剩余票数 : 1
剩余票数 : 1
用户 : 432 抢票成功
用户 : 2636 抢票失败
用户 : 7772 抢票失败
用户 : 1272 抢票失败
'''
加锁之后, 一张票只有一个人能抢成功, 其实就是让抢票这个局部环节变成了串行, 谁抢到了就谁用, 牺牲了效率, 提升了数据安全性
2.总结
- 以上加锁的操作方法可以保证多个进程修改同一份数据时保证数据的安全性, 即串行的修改, 但是也带来了一些问题 :
1、共享的数据基于文件, 文件又属于硬盘, 效率就比较低
2、需要自己加锁和解锁操作, 这是一件非常危险的操作, 如果忘记解锁程序就停在原地
- 因此我们就需要一种兼顾效率以及能自动帮我们处理锁的问题的这种介质
🍑需求
1、多个进程共享同一块内存数据, 实现高效率
2、找到一个能帮我们处理好锁的问题的机制 : multiprocessing模块为我们提供了IPC通信机制:管道和队列
🍑介质
1、管道和队列, 基于内存中的空间存放数据
2、队列是基于管道和锁实现的, 可以让我们从复杂的锁问题中解脱出来
ps : 我们应该尽量避免使用共享数据, (比如一个文件的传递应该将文件保存到硬盘, 在管道中放的应该是一个路径, 而不应该是一个完整的文件), 尽可能使用消息传递和队列, 避免处理复杂 的同步和锁问题, 而且在进程数目增多时, 往往可以获得更好的可扩展性
十一.进程间通信 (IPC)
进程间通信机制简称 IPC (Inter Process Communication)
进程间彼此隔离, 要实现 IPC, multiprocessing 模块为我们提供了队列和管道这两种形式
1.什么是管道, 什么是队列
- 管道(了解即可) : 一个进程将一个数据放入管道内(共享内存), 另一个进程从管道内取出数据进行处理
- 队列 : 管道加锁, 先进先出, 帮我们实现了复杂的加锁解锁操作, 以下我们主要介绍队列的使用
2.队列的常用方法
- 创建一个队列实例
🍑导入模块
from multiprocessing import Queue
🍑创建一个队列对象
q = Queue([maxsize]) # 多进程可以使用Queue进行数据传递
🍑参数介绍
maxsize # 是队列中允许最大项, 省略则无大小限制
- 常用方法介绍
方法 | 功能 |
---|---|
q.put( ) | 向队列中传入数据,可选参数 : blocked(锁定状态)和timeout(超时时间)。如果blocked为True(默认值), 并且timeout为正值, 该方法会阻塞timeout指定的时间, 直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常 |
q.get( ) | 从队列读取走一个元素, 有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常 |
q.get_nowait( ) | 同q.get(blocked=False) |
q.put_nowait( ) | 同q.put(blocked=False) |
q.empty( ) | 调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目 |
q.full( ) | 调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走 |
q.qsize( ) | 返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样 |
- 其他方法介绍
方法 | 作用 |
---|---|
q.cancel_join_thread( ) | 不会在进程退出时自动连接后台线程, 可以防止join_thread()方法阻塞 |
q.close( ) | 关闭队列, 防止队列中加入更多数据。调用此方法,后台线程将继续写入那些已经入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将调用此方法。关闭队列不会在队列使用者中产生任何类型的数据结束信号或异常。例如,如果某个使用者正在被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误 |
q.join_thread( ) | 连接队列的后台线程。此方法用于在调用q.close()方法之后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread方法可以禁止这种行为 |
3.队列的使用
from multiprocessing import Queue
q = Queue(3) # 创建一个队列,设置最大项为3
q.put({"name":"ii"}) # 放入一个字典
q.put([1,2,3,4,5]) # 放入一个列表
q.put("shawn") # 放入一个字符串
try:
# q.put(1777,block=True,timeout=3)
q.put(1777,block=False) # 放入一个整形,并设置队列已满立马抛异常
except Exception:
print("队列已满")
print(q.get()) # 取一个值
print(q.get()) # 2
print(q.get()) # 3
try:
# print(q.get(block=True,timeout=3))
print(q.get(block=False)) # 取一个值,队列为空立马抛出异常
except Exception:
print("队列已空")
'''输出
队列已满
{'namwe': 'ahsns'}
[1, 2, 3, 4, 5]
shawn
队列已空
'''