马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
1. 历程同步的须要性
在多历程编程中,当多个历程须要访问共享资源时,会出现竞争条件标题。比方火车票售卖体系中,如果多个售票窗口同时读取和修改剩余票数,大概导致数据差别等。
1.1 竞争条件示例
- from multiprocessing import Process, Value
- def sell_tickets(tickets):
- for _ in range(1000):
- tickets.value -= 1 # 非原子操作
- if __name__ == '__main__':
- tickets = Value('i', 10000)
- processes = [Process(target=sell_tickets, args=(tickets,))
- for _ in range(10)]
-
- for p in processes:
- p.start()
- for p in processes:
- p.join()
-
- print(f"剩余票数: {tickets.value}") # 结果不确定,可能大于0
复制代码 在多历程环境下,多个历程可以同时实行。像代码里的tickets.value -= 1利用,现实上并非原子利用。它包罗读取tickets.value的值、对该值减 1、再把新值写回tickets.value这几个步调。当多个历程同时实行这一利用时,就大概出现数据差别等的状态。好比,两个历程同时读取到剩余票数为 100,各自减 1 后都写回 99,而现实上应该是 98。
2. 锁(Lock)机制
锁机制可以包管同一时间只有一个历程可以或许访问共享资源。当一个历程获取到锁之后,其他历程就必须等候该历程开释锁,才气继承访问共享资源。如许就能制止多个历程同时修改共享资源,从而办理竞争条件标题。
2.1 底子锁使用
- from multiprocessing import Process, Lock, Value
- def sell_tickets(tickets, lock):
- for _ in range(1000):
- lock.acquire()
- tickets.value -= 1
- lock.release()
- if __name__ == '__main__':
- tickets = Value('i', 10000)
- lock = Lock()
- processes = [Process(target=sell_tickets, args=(tickets, lock))
- for _ in range(10)]
-
- for p in processes:
- p.start()
- for p in processes:
- p.join()
-
- print(f"剩余票数: {tickets.value}")
复制代码 在上述代码中,lock.acquire()用于获取锁,一旦某个历程获取到锁,其他历程就只能等候。tickets.value -= 1是对共享资源的修改利用,由于有锁的掩护,同一时间只有一个历程能实行这一利用。lock.release()用于开释锁,开释后其他历程就可以竞争获取锁。
2.2 上下文管理器简化
- from multiprocessing import Process, Lock, Value
- def sell_tickets(tickets, lock):
- for _ in range(1000):
- with lock: # 自动获取和释放锁
- tickets.value -= 1
- if __name__ == '__main__':
- tickets = Value('i', 10000)
- lock = Lock()
- processes = [Process(target=sell_tickets, args=(tickets, lock))
- for _ in range(10)]
-
- for p in processes:
- p.start()
- for p in processes:
- p.join()
-
- print(f"剩余票数: {tickets.value}")
复制代码 使用with lock语句,Python 会自动帮我们获取和开释锁。进入with语句块时,会自动调用lock.acquire()获取锁;脱离with语句块时,会自动调用lock.release()开释锁。如允许以让代码更加轻巧,也能制止忘记开释锁的标题。
2.3 银行转账案例
原始标题代码(存在竞争条件)
- from multiprocessing import Process, Value
- import time
- def transfer(balance, amount):
- # 没有锁保护的转账操作
- current = balance.value
- time.sleep(0.01) # 模拟处理延迟
- balance.value = current + amount # 非原子操作
- if __name__ == '__main__':
- balance = Value('i', 100) # 初始余额100
-
- # 创建100个存款和100个取款进程
- processes = []
- for _ in range(100):
- p_deposit = Process(target=transfer, args=(balance, 1)) # 存1元
- p_withdraw = Process(target=transfer, args=(balance, -1)) # 取1元
- processes.extend([p_deposit, p_withdraw])
-
- for p in processes:
- p.start()
- for p in processes:
- p.join()
-
- print(f"最终余额: {balance.value}") # 预期应该是100,但实际结果不确定
复制代码 标题分析:
- 多个历程同时读取和修改balance.value会导致竞争条件
- balance.value = current + amount不是原子利用
- 由于time.sleep放大了竞争窗口,标题更容易显现
- 终极余额大概不即是初始值100,阐明出现了数据差别等
改进后的办理方案(使用Lock)
- from multiprocessing import Process, Value, Lock
- import time
- def transfer(balance, lock, amount):
- with lock: # 使用上下文管理器自动获取和释放锁
- current = balance.value
- time.sleep(0.01) # 模拟处理延迟
- balance.value = current + amount
- if __name__ == '__main__':
- balance = Value('i', 100) # 初始余额100
- lock = Lock() # 创建锁对象
-
- processes = []
- for _ in range(100):
- p_deposit = Process(target=transfer, args=(balance, lock, 1)) # 存1元
- p_withdraw = Process(target=transfer, args=(balance, lock, -1)) # 取1元
- processes.extend([p_deposit, p_withdraw])
-
- for p in processes:
- p.start()
- for p in processes:
- p.join()
-
- print(f"最终余额: {balance.value}") # 现在总能正确输出100
复制代码 改进点阐明:
- 引入Lock对象包管对共享变量的互斥访问
- 使用with lock:上下文管理器简化锁的获取和开释
- 确保读取-修改-写入利用成为原子利用
- 纵然有耽误,也能包管数据同等性
实行效果对比
版本运行次数大概效果阐明原始版本10次90-110之间随机因竞争条件效果不确定改进版本10次每次都是100包管数据同等性可视化标题发生过程
- 时间线 进程A(存款) 进程B(取款) 余额值
- --------------------------------------------------------------------
- t1 读取balance=100
- t2 读取balance=100
- t3 计算100+1=101
- t4 计算100-1=99
- t5 写入101
- t6 写入99
复制代码 由于没有同步机制,后写入的利用会覆盖前一个利用,导致存款利用"丢失"。
更优实践发起
对于这种计数器场景,可以使用Value的原子利用:- from multiprocessing import Value
- balance = Value('i', 100)
- balance.value += 1 # 非原子操作(不安全)
- balance.get_lock().acquire()
- balance.value += 1 # 原子操作(安全)
- balance.get_lock().release()
- # 或者使用with语句
- with balance.get_lock():
- balance.value += 1
复制代码 如允许以直接使用共享变量自带的锁,代码更轻巧。
3. 信号量(Semaphore)
信号量是更高级的同步原语,答应指定命目的历程同时访问资源。
3.1 底子信号量
- from multiprocessing import Semaphore
- s = Semaphore(3) # 允许3个进程同时访问
- s.acquire()
- print("进程1进入")
- s.acquire()
- print("进程2进入")
- s.acquire()
- print("进程3进入")
- # 此时第4个进程会被阻塞
复制代码 3.2 课堂钥匙案例
- from multiprocessing import Process, Semaphore
- import time
- import random
- def student(id, sem):
- with sem:
- print(f"学生{id}进入教室")
- time.sleep(random.uniform(1, 3))
- print(f"学生{id}离开教室")
- if __name__ == '__main__':
- sem = Semaphore(5) # 5把钥匙
- students = [Process(target=student, args=(i, sem))
- for i in range(20)]
-
- for s in students:
- s.start()
- for s in students:
- s.join()
复制代码 4. 变乱(Event)机制
变乱用于历程间的简朴通讯,一个历程发出"变乱已发生"信号,其他历程等候该变乱。
4.1 变乱底子
- from multiprocessing import Event
- e = Event()
- print(e.is_set()) # False
- e.set()
- print(e.is_set()) # True
- e.wait() # 立即通过
- e.clear()
- e.wait() # 阻塞直到set()被调用
复制代码 4.2 红绿灯控制案例
- from multiprocessing import Process, Event
- import time
- def traffic_light(e):
- while True:
- print("\033[32m绿灯亮\033[0m" if e.is_set() else "\033[31m红灯亮\033[0m")
- time.sleep(2)
- e.set() if not e.is_set() else e.clear()
- def car(e, car_id):
- e.wait()
- print(f"汽车{car_id}通过路口")
- if __name__ == '__main__':
- e = Event()
- light = Process(target=traffic_light, args=(e,))
- light.daemon = True # 守护进程,主进程结束自动退出
- light.start()
-
- for i in range(10):
- time.sleep(0.5)
- Process(target=car, args=(e, i)).start()
复制代码 5. 综合案例:火车票售卖体系
5.1 共享内存实现
- from multiprocessing import Process, Value, Lock
- import time
- class TicketSeller(Process):
- def __init__(self, tickets, lock, name):
- super().__init__()
- self.tickets = tickets
- self.lock = lock
- self.name = name
-
- def run(self):
- while True:
- with self.lock:
- if self.tickets.value <= 0:
- break
- time.sleep(0.1) # 模拟处理时间
- print(f"{self.name}售出票号{self.tickets.value}")
- self.tickets.value -= 1
- if __name__ == '__main__':
- tickets = Value('i', 100)
- lock = Lock()
-
- sellers = [TicketSeller(tickets, lock, f"窗口{i}")
- for i in range(1, 4)]
-
- for s in sellers:
- s.start()
- for s in sellers:
- s.join()
-
- print("所有票已售完")
复制代码 5.2 文件共享实现
- from multiprocessing import Process, Lock
- import time
- def sell_tickets(lock, window):
- while True:
- with lock:
- with open("tickets.txt", "r+") as f:
- tickets = int(f.read())
- if tickets <= 0:
- break
- time.sleep(0.1)
- print(f"{window}售出票号{tickets}")
- f.seek(0)
- f.write(str(tickets-1))
- if __name__ == '__main__':
- lock = Lock()
-
- # 初始化票数
- with open("tickets.txt", "w") as f:
- f.write("100")
-
- processes = [Process(target=sell_tickets, args=(lock, f"窗口{i}"))
- for i in range(1, 4)]
-
- for p in processes:
- p.start()
- for p in processes:
- p.join()
-
- print("所有票已售完")
复制代码 6. 同步机制选择指南
机制实用场景优点缺点Lock互斥访问共享资源简朴直接大概引起死锁Semaphore限定资源访问数量机动控制并发度比锁复杂Event历程间简朴关照轻量级关照机制不通报数据7. 常见标题解答
Q1: 锁和信号量的区别是什么?
- 锁是二元信号量(0/1),信号量可以设置多个允许
- 锁用于互斥访问,信号量用于控制资源访问数量
Q2: 怎样制止死锁?
Q3: 文件共享和共享内存哪个更好?
- 共享内存更快但更复杂
- 文件共享更简朴但性能较低
- 根据场景选择符合方案
|