Python多历程同步全分析:从竞争条件到锁、信号量的实战应用

[复制链接]
发表于 2025-9-27 16:36:57 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
1. 历程同步的须要性

在多历程编程中,当多个历程须要访问共享资源时,会出现竞争条件标题。比方火车票售卖体系中,如果多个售票窗口同时读取和修改剩余票数,大概导致数据差别等。
1.1 竞争条件示例
  1. from multiprocessing import Process, Value
  2. def sell_tickets(tickets):
  3.     for _ in range(1000):
  4.         tickets.value -= 1  # 非原子操作
  5. if __name__ == '__main__':
  6.     tickets = Value('i', 10000)
  7.     processes = [Process(target=sell_tickets, args=(tickets,))
  8.                 for _ in range(10)]
  9.    
  10.     for p in processes:
  11.         p.start()
  12.     for p in processes:
  13.         p.join()
  14.    
  15.     print(f"剩余票数: {tickets.value}")  # 结果不确定,可能大于0
复制代码
在多历程环境下,多个历程可以同时实行。像代码里的tickets.value -= 1利用,现实上并非原子利用。它包罗读取tickets.value的值、对该值减 1、再把新值写回tickets.value这几个步调。当多个历程同时实行这一利用时,就大概出现数据差别等的状态。好比,两个历程同时读取到剩余票数为 100,各自减 1 后都写回 99,而现实上应该是 98。
2. 锁(Lock)机制

锁机制可以包管同一时间只有一个历程可以或许访问共享资源。当一个历程获取到锁之后,其他历程就必须等候该历程开释锁,才气继承访问共享资源。如许就能制止多个历程同时修改共享资源,从而办理竞争条件标题。
2.1 底子锁使用
  1. from multiprocessing import Process, Lock, Value
  2. def sell_tickets(tickets, lock):
  3.     for _ in range(1000):
  4.         lock.acquire()
  5.         tickets.value -= 1
  6.         lock.release()
  7. if __name__ == '__main__':
  8.     tickets = Value('i', 10000)
  9.     lock = Lock()
  10.     processes = [Process(target=sell_tickets, args=(tickets, lock))
  11.                 for _ in range(10)]
  12.    
  13.     for p in processes:
  14.         p.start()
  15.     for p in processes:
  16.         p.join()
  17.    
  18.     print(f"剩余票数: {tickets.value}")
复制代码
在上述代码中,lock.acquire()用于获取锁,一旦某个历程获取到锁,其他历程就只能等候。tickets.value -= 1是对共享资源的修改利用,由于有锁的掩护,同一时间只有一个历程能实行这一利用。lock.release()用于开释锁,开释后其他历程就可以竞争获取锁。
2.2 上下文管理器简化
  1. from multiprocessing import Process, Lock, Value
  2. def sell_tickets(tickets, lock):
  3.     for _ in range(1000):
  4.         with lock:  # 自动获取和释放锁
  5.             tickets.value -= 1
  6. if __name__ == '__main__':
  7.     tickets = Value('i', 10000)
  8.     lock = Lock()
  9.     processes = [Process(target=sell_tickets, args=(tickets, lock))
  10.                 for _ in range(10)]
  11.    
  12.     for p in processes:
  13.         p.start()
  14.     for p in processes:
  15.         p.join()
  16.    
  17.     print(f"剩余票数: {tickets.value}")
复制代码
使用with lock语句,Python 会自动帮我们获取和开释锁。进入with语句块时,会自动调用lock.acquire()获取锁;脱离with语句块时,会自动调用lock.release()开释锁。如允许以让代码更加轻巧,也能制止忘记开释锁的标题。
2.3 银行转账案例

原始标题代码(存在竞争条件)
  1. from multiprocessing import Process, Value
  2. import time
  3. def transfer(balance, amount):
  4.     # 没有锁保护的转账操作
  5.     current = balance.value
  6.     time.sleep(0.01)  # 模拟处理延迟
  7.     balance.value = current + amount  # 非原子操作
  8. if __name__ == '__main__':
  9.     balance = Value('i', 100)  # 初始余额100
  10.    
  11.     # 创建100个存款和100个取款进程
  12.     processes = []
  13.     for _ in range(100):
  14.         p_deposit = Process(target=transfer, args=(balance, 1))  # 存1元
  15.         p_withdraw = Process(target=transfer, args=(balance, -1)) # 取1元
  16.         processes.extend([p_deposit, p_withdraw])
  17.    
  18.     for p in processes:
  19.         p.start()
  20.     for p in processes:
  21.         p.join()
  22.    
  23.     print(f"最终余额: {balance.value}")  # 预期应该是100,但实际结果不确定
复制代码
标题分析:

  •         多个历程同时读取和修改balance.value会导致竞争条件
  •         balance.value = current + amount不是原子利用
  •         由于time.sleep放大了竞争窗口,标题更容易显现
  •         终极余额大概不即是初始值100,阐明出现了数据差别等
改进后的办理方案(使用Lock)
  1. from multiprocessing import Process, Value, Lock
  2. import time
  3. def transfer(balance, lock, amount):
  4.     with lock:  # 使用上下文管理器自动获取和释放锁
  5.         current = balance.value
  6.         time.sleep(0.01)  # 模拟处理延迟
  7.         balance.value = current + amount
  8. if __name__ == '__main__':
  9.     balance = Value('i', 100)  # 初始余额100
  10.     lock = Lock()  # 创建锁对象
  11.    
  12.     processes = []
  13.     for _ in range(100):
  14.         p_deposit = Process(target=transfer, args=(balance, lock, 1))  # 存1元
  15.         p_withdraw = Process(target=transfer, args=(balance, lock, -1)) # 取1元
  16.         processes.extend([p_deposit, p_withdraw])
  17.    
  18.     for p in processes:
  19.         p.start()
  20.     for p in processes:
  21.         p.join()
  22.    
  23.     print(f"最终余额: {balance.value}")  # 现在总能正确输出100
复制代码
改进点阐明:

  •         引入Lock对象包管对共享变量的互斥访问
  •         使用with lock:上下文管理器简化锁的获取和开释
  •         确保读取-修改-写入利用成为原子利用
  •         纵然有耽误,也能包管数据同等性
实行效果对比

版本运行次数大概效果阐明原始版本10次90-110之间随机因竞争条件效果不确定改进版本10次每次都是100包管数据同等性可视化标题发生过程
  1. 时间线       进程A(存款)               进程B(取款)              余额值
  2. --------------------------------------------------------------------
  3. t1      读取balance=100
  4. t2                          读取balance=100
  5. t3      计算100+1=101
  6. t4                          计算100-1=99
  7. t5      写入101
  8. t6                          写入99
复制代码
由于没有同步机制,后写入的利用会覆盖前一个利用,导致存款利用"丢失"。
更优实践发起

对于这种计数器场景,可以使用Value的原子利用:
  1. from multiprocessing import Value
  2. balance = Value('i', 100)
  3. balance.value += 1  # 非原子操作(不安全)
  4. balance.get_lock().acquire()
  5. balance.value += 1  # 原子操作(安全)
  6. balance.get_lock().release()
  7. # 或者使用with语句
  8. with balance.get_lock():
  9.     balance.value += 1
复制代码
如允许以直接使用共享变量自带的锁,代码更轻巧。
3. 信号量(Semaphore)

信号量是更高级的同步原语,答应指定命目的历程同时访问资源。
3.1 底子信号量
  1. from multiprocessing import Semaphore
  2. s = Semaphore(3)  # 允许3个进程同时访问
  3. s.acquire()
  4. print("进程1进入")
  5. s.acquire()
  6. print("进程2进入")
  7. s.acquire()
  8. print("进程3进入")
  9. # 此时第4个进程会被阻塞
复制代码
3.2 课堂钥匙案例
  1. from multiprocessing import Process, Semaphore
  2. import time
  3. import random
  4. def student(id, sem):
  5.     with sem:
  6.         print(f"学生{id}进入教室")
  7.         time.sleep(random.uniform(1, 3))
  8.         print(f"学生{id}离开教室")
  9. if __name__ == '__main__':
  10.     sem = Semaphore(5)  # 5把钥匙
  11.     students = [Process(target=student, args=(i, sem))
  12.                for i in range(20)]
  13.    
  14.     for s in students:
  15.         s.start()
  16.     for s in students:
  17.         s.join()
复制代码
4. 变乱(Event)机制

变乱用于历程间的简朴通讯,一个历程发出"变乱已发生"信号,其他历程等候该变乱。
4.1 变乱底子
  1. from multiprocessing import Event
  2. e = Event()
  3. print(e.is_set())  # False
  4. e.set()
  5. print(e.is_set())  # True
  6. e.wait()  # 立即通过
  7. e.clear()
  8. e.wait()  # 阻塞直到set()被调用
复制代码
4.2 红绿灯控制案例
  1. from multiprocessing import Process, Event
  2. import time
  3. def traffic_light(e):
  4.     while True:
  5.         print("\033[32m绿灯亮\033[0m" if e.is_set() else "\033[31m红灯亮\033[0m")
  6.         time.sleep(2)
  7.         e.set() if not e.is_set() else e.clear()
  8. def car(e, car_id):
  9.     e.wait()
  10.     print(f"汽车{car_id}通过路口")
  11. if __name__ == '__main__':
  12.     e = Event()
  13.     light = Process(target=traffic_light, args=(e,))
  14.     light.daemon = True  # 守护进程,主进程结束自动退出
  15.     light.start()
  16.    
  17.     for i in range(10):
  18.         time.sleep(0.5)
  19.         Process(target=car, args=(e, i)).start()
复制代码
5. 综合案例:火车票售卖体系

5.1 共享内存实现
  1. from multiprocessing import Process, Value, Lock
  2. import time
  3. class TicketSeller(Process):
  4.     def __init__(self, tickets, lock, name):
  5.         super().__init__()
  6.         self.tickets = tickets
  7.         self.lock = lock
  8.         self.name = name
  9.         
  10.     def run(self):
  11.         while True:
  12.             with self.lock:
  13.                 if self.tickets.value <= 0:
  14.                     break
  15.                 time.sleep(0.1)  # 模拟处理时间
  16.                 print(f"{self.name}售出票号{self.tickets.value}")
  17.                 self.tickets.value -= 1
  18. if __name__ == '__main__':
  19.     tickets = Value('i', 100)
  20.     lock = Lock()
  21.    
  22.     sellers = [TicketSeller(tickets, lock, f"窗口{i}")
  23.               for i in range(1, 4)]
  24.    
  25.     for s in sellers:
  26.         s.start()
  27.     for s in sellers:
  28.         s.join()
  29.    
  30.     print("所有票已售完")
复制代码
5.2 文件共享实现
  1. from multiprocessing import Process, Lock
  2. import time
  3. def sell_tickets(lock, window):
  4.     while True:
  5.         with lock:
  6.             with open("tickets.txt", "r+") as f:
  7.                 tickets = int(f.read())
  8.                 if tickets <= 0:
  9.                     break
  10.                 time.sleep(0.1)
  11.                 print(f"{window}售出票号{tickets}")
  12.                 f.seek(0)
  13.                 f.write(str(tickets-1))
  14. if __name__ == '__main__':
  15.     lock = Lock()
  16.    
  17.     # 初始化票数
  18.     with open("tickets.txt", "w") as f:
  19.         f.write("100")
  20.    
  21.     processes = [Process(target=sell_tickets, args=(lock, f"窗口{i}"))
  22.                 for i in range(1, 4)]
  23.    
  24.     for p in processes:
  25.         p.start()
  26.     for p in processes:
  27.         p.join()
  28.    
  29.     print("所有票已售完")
复制代码
6. 同步机制选择指南

机制实用场景优点缺点Lock互斥访问共享资源简朴直接大概引起死锁Semaphore限定资源访问数量机动控制并发度比锁复杂Event历程间简朴关照轻量级关照机制不通报数据7. 常见标题解答

Q1: 锁和信号量的区别是什么?

  •         锁是二元信号量(0/1),信号量可以设置多个允许
  •         锁用于互斥访问,信号量用于控制资源访问数量
Q2: 怎样制止死锁?

  •         按固定序次获取多个锁
  •         使用超机遇制
  •         制止嵌套锁
Q3: 文件共享和共享内存哪个更好?

  •         共享内存更快但更复杂
  •         文件共享更简朴但性能较低
  •         根据场景选择符合方案

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表