【4.3】进程间同步(互斥锁)
【一】什么是进程同步(互斥锁)
-
互斥锁(Mutex)是一种用于多线程编程中控制对共享资源访问的机制。
-
其作用是保证在同一时刻只有一个线程在访问共享资源,从而避免多个线程同时读写数据造成的问题。
-
互斥锁的基本原理是在对共享资源进行访问前加锁,使得其他线程无法访问该资源,当访问完成后再解锁,使得其他线程可以进行访问。
-
通过这种方式,可以保证同一时间只有一个线程在执行关键代码段,从而保证了数据的安全性。
-
需要注意的是,互斥锁会带来一些额外的开销,
-
【二】多个进程共享同一打印终端
- 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,
- 而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理
【1】未加锁
- 并发运行,效率高,但竞争同一打印终端,带来了打印错乱
# 并发运行,效率高,但竞争同一打印终端,带来了打印错乱
from multiprocessing import Process
import os, time
def work():
print('%s is running' % os.getpid())
time.sleep(2)
print('%s is done' % os.getpid())
if __name__ == '__main__':
for i in range(3):
p = Process(target=work)
p.start()
# 86789 is running
# 86788 is running
# 86790 is running
# 86789 is done
# 86788 is done
# 86790 is done
【2】加锁
- 由并发变成了串行,牺牲了运行效率,但避免了竞争
from multiprocessing import Process, Lock
import os
import time
def work(lock):
# 【一】加锁,限制只能一个进程使用
lock.acquire()
print('%s is running' % os.getpid())
time.sleep(2)
print('%s is done' % os.getpid())
# 【二】释放锁,将唯一限制解除,其他进程可进行操作
lock.release()
if __name__ == '__main__':
# 【一】创建锁对象,这里的锁对象对于所有主进程和子进程来说都是唯一的,都是同一把锁
lock = Lock()
# 【二】创建一个子进程的列表
processes = []
# 【三】创建子进程
for i in range(3):
# 目标为加锁的函数,参数为同一把锁对象
p = Process(target=work, args=(lock,))
# 添加到子进程列表中
processes.append(p)
# 启动子进程
p.start()
# 最后,我们使用另一个循环来等待所有的进程完成。
# 在每次迭代中,我们都检查是否有任何尚未完成的进程。
# 如果有,我们就调用其 join() 方法,等待它完成。
# 只有当所有的进程都已经完成之后,程序才会退出。
for process in processes:
process.join()
# 87271 is running
# 87271 is done
# 87272 is running
# 87272 is done
# 87270 is running
# 87270 is done
【三】多个进程共享同一文件
- 文件当数据库,模拟抢票
- 并发运行,效率高,但竞争写同一文件,数据写入错乱
【1】未加锁
import random
from multiprocessing import Process, Lock
import time
import json
import os
# 【一】初始化数据库:如果文件不存在则新建并写入数据
# 文件db的内容为:{"count":1}
db_path = os.path.join(os.path.dirname(__file__), 'ticket_data.json')
def init_data():
with open(file=db_path, mode='w', encoding='utf-8') as fp:
json.dump({'ticket_number': 2}, fp)
# 【二】拿到票的数据
def get_ticket():
with open(db_path, 'r', encoding='utf8') as f:
ticket_dict = json.load(f)
return ticket_dict
# 【三】存储票的数据
def save_ticket(ticket_dict):
with open(db_path, 'w', encoding='utf8') as f:
json.dump(ticket_dict, f)
# (1)查票
def search_ticket(name):
# 模拟从数据库读取票数
# 打开文件读取票数
ticket_dict = get_ticket()
print(f'用户:>>>{name} 正在查询余票:>>>{ticket_dict.get("ticket_number")}')
# (2)买票
def buy_ticket(name):
# (2.1)先查票
ticket_dict = get_ticket()
# (2.2)模拟网络延迟
time.sleep(random.randint(1, 3))
# (2.3) 判断当前是否有票
if ticket_dict.get('ticket_number') > 0:
# (2.5)有票再买票
ticket_dict['ticket_number'] -= 1
save_ticket(ticket_dict)
print(f'用户:>>>{name} 买票成功!!')
else:
# (2.4) 没票则反馈信息
print(f'当前无余票!!')
# 整合成一个人的功能
def main(name):
# 所有人都去查票
search_ticket(name)
# 所有人都能随便买票
buy_ticket(name)
if __name__ == '__main__':
# 初始化 票数
init_data()
p_list = []
# 多进程演示 - 多个人购票
for i in range(1, 5):
p = Process(target=main, args=(i,))
p.start()
p_list.append(p)
for p in p_list:
p.join()
# 所有人都能查票成功,所有人都能购票成功
# 用户:>>>1 正在查询余票:>>>2
# 用户:>>>4 正在查询余票:>>>2
# 用户:>>>3 正在查询余票:>>>2
# 用户:>>>2 正在查询余票:>>>2
# 用户:>>>3 买票成功!!
# 用户:>>>1 买票成功!!
# 用户:>>>4 买票成功!!
# 用户:>>>2 买票成功!!
【2】加锁
-
针对上述数据错乱问题,解决方式就是加锁处理
- 将并发变成串行牺牲效率,但是保证了数据的安全
-
购票行为由并发变成了串行,牺牲了运行效率,但保证了数据安全
import random
from multiprocessing import Process, Lock
import time
import json
import os
# 【一】初始化数据库:如果文件不存在则新建并写入数据
# 文件db的内容为:{"count":1}
db_path = os.path.join(os.path.dirname(__file__), 'ticket_data.json')
def init_data():
with open(file=db_path, mode='w', encoding='utf-8') as fp:
json.dump({'ticket_number': 2}, fp)
# 【二】拿到票的数据
def get_ticket():
with open(db_path, 'r', encoding='utf8') as f:
ticket_dict = json.load(f)
return ticket_dict
# 【三】存储票的数据
def save_ticket(ticket_dict):
with open(db_path, 'w', encoding='utf8') as f:
json.dump(ticket_dict, f)
# (1)查票
def search_ticket(name):
# 模拟从数据库读取票数
# 打开文件读取票数
ticket_dict = get_ticket()
print(f'用户:>>>{name} 正在查询余票:>>>{ticket_dict.get("ticket_number")}')
# (2)买票
def buy_ticket(name):
# (2.1)先查票
ticket_dict = get_ticket()
# (2.2)模拟网络延迟
time.sleep(random.randint(1, 3))
# (2.3) 判断当前是否有票
if ticket_dict.get('ticket_number') > 0:
# (2.5)有票再买票
ticket_dict['ticket_number'] -= 1
save_ticket(ticket_dict)
print(f'用户:>>>{name} 买票成功!!')
else:
# (2.4) 没票则反馈信息
print(f'当前无余票!!')
# 整合成一个人的功能
def main(name, mutex):
# 所有人都去查票
search_ticket(name)
# 给买票环节加锁处理
# (1)先强锁
mutex.acquire()
buy_ticket(name)
# (2)抢完后释放锁
mutex.release()
if __name__ == '__main__':
# 初始化 票数
init_data()
# 在主进程中生成一把锁,让所有的子进程去抢,谁抢到谁就先买票
mutex = Lock()
p_list = []
# 多进程演示 - 多个人购票
for i in range(1, 5):
p = Process(target=main, args=(i, mutex))
p.start()
p_list.append(p)
for p in p_list:
p.join()
# 所有人都能查票成功,但是只有前两名购票成功
# 前两名是随机抢到的,不一定是按顺序(考虑到各种因素,如网络等)
# 用户:>>>3 正在查询余票:>>>2
# 用户:>>>2 正在查询余票:>>>2
# 用户:>>>4 正在查询余票:>>>2
# 用户:>>>1 正在查询余票:>>>2
# 用户:>>>3 买票成功!!
# 用户:>>>2 买票成功!!
# 当前无余票!!
# 当前无余票!!
【四】互斥锁的优缺点
【1】加锁的优点
- 加锁可以保证多个进程修改同一块数据时
- 同一时间只能有一个任务可以进行修改,即串行的修改
- 没错,速度是慢了,但牺牲了速度却保证了数据安全。
【2】加锁的缺点
- 虽然可以用文件共享数据实现进程间通信,但问题是:
- 1.效率低(共享数据基于文件,而文件是硬盘上的数据)
- 2.需要自己加锁处理
【3】优化方案
-
因此我们最好找寻一种解决方案能够兼顾:
- 1、效率高(多个进程共享一块内存的数据)
- 2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
-
1 队列和管道都是将数据存放于内存中
-
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
-
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
【五】行锁和表锁
行锁和表锁是数据库中常用的锁定机制。
【1】行锁
- 行锁是在对数据库表中的某个数据行进行修改时
- 一次只允许一个用户操作该行
- 其他用户如果需要修改该行数据就必须等待。
- 通过行锁定可以避免多个用户同时修改同一行数据所导致的数据不一致问题。
【2】表锁
- 表锁则是当一个用户操作某个数据库表时
- 会锁定整个表,其他用户同时不能操作该表。
- 这在一些特殊场景下比如表维护、备份等是非常有用的。
【3】小结
- 总的来说
- 行锁定是比较细粒度的锁定
- 而表锁定则是更为粗粒度的锁定方法。
【六】特别提醒
- 锁不要轻易使用,容易造成死锁现象
- 锁只在处理数据的部分加,用来保证数据的安全(只在争抢数据的环节加锁)
本文来自博客园,作者:Chimengmeng,转载请注明原文链接:https://www.cnblogs.com/dream-ze/p/17982368