Python线程安全问题及解决方案
·
线程安全概念
线程安全指在多线程环境中,函数或库能正确处理共享变量,确保程序功能正确执行。当多个线程并发操作共享资源时,若未同步执行,线程切换可能导致不可预料的结果。例如,在银行取款场景中,两个线程同时操作同一账户余额,可能因读取过时数据而出现负数余额,违背业务逻辑。
1、线程不安全示例
假设账户余额初始为 $1000$,两个线程均尝试取款 $800$:
- 线程1执行流程:
- 读取余额 $1000$,判断 $1000 \geq 800$(成立)。
- 执行扣减: $1000 - 800 = 200$,更新余额为 $200$。
- 线程2执行流程:
- 读取余额 $1000$(线程1操作前的值),判断 $1000 \geq 800$(成立)。
- 执行扣减: $200 - 800 = -600$,导致余额为负。
此问题源于临界区代码(如余额判断与扣减)未加锁,允许多线程并发执行,破坏数据一致性。
2、Lock解决方案
Python的 threading.Lock 通过互斥锁确保同一时间仅一个线程执行临界区代码。以下是两种标准用法:
1. try-finally 模式
显式管理锁生命周期:手动获取锁,在 try 块执行临界区代码,在 finally 块强制释放锁,避免死锁。
import threading
lock = threading.Lock()
lock.acquire()
try:
# 临界区代码
if account.balance >= amount:
account.balance -= amount
finally:
lock.release()
2. with 模式(推荐)
语法糖自动管理锁:进入 with 块时加锁,退出时解锁,代码简洁且避免漏释放。
import threading
lock = threading.Lock()
with lock:
# 临界区代码
if account.balance >= amount:
account.balance -= amount
实例演示与测试
以下代码模拟银行取款场景,对比线程不安全与安全方案:
代码实现
# 1. 导入Python自带的多线程工具:用来创建多个同时运行的线程
import threading
# 2. 导入Python自带的时间工具:用来让程序暂停一会儿
import time
# 3. 定义一个账户类:模拟银行的账户,用来存余额
class Account:
# 4. 初始化方法:创建账户时,给账户设置初始余额
def __init__(self, balance=0):
# 5. 把传入的余额,存到账户对象里(这是共享的钱,多个线程都会操作它)
self.balance = balance
# 6. 定义【不安全】的取款函数:会出现超支bug
# account:要操作的账户,amount:要取多少钱
def unsafe_draw(account, amount):
# 7. 判断:账户里的钱 >= 要取的钱吗?
if account.balance >= amount:
# 8. 程序暂停0.1秒:故意制造线程切换,触发线程安全问题
time.sleep(0.1)
# 9. 扣钱:账户余额 = 余额 - 取款金额
account.balance -= amount
# 10. 打印提示:取款成功,显示当前余额
print(f"取款成功!当前余额:{account.balance}")
# 11. 如果余额不够,执行这里
else:
# 12. 打印提示:余额不足,显示当前余额
print(f"余额不足!当前余额:{account.balance}")
# 13. 创建一把线程锁:用来保护账户钱不被乱扣(同一时间只允许一个线程操作)
lock1 = threading.Lock()
# 14. 定义【第一种安全】的取款函数:用try-finally管理锁
def safe_draw_try(account, amount):
# 15. 手动加锁:抢到锁的线程才能往下执行,没抢到的等着
lock1.acquire()
try:
# 16. 判断余额够不够取款
if account.balance >= amount:
# 17. 暂停0.1秒,模拟业务延迟
time.sleep(0.1)
# 18. 安全扣钱
account.balance -= amount
# 19. 打印取款成功提示
print(f"try方案:取款成功!当前余额:{account.balance}")
# 20. 余额不足时执行
else:
# 21. 打印余额不足提示
print(f"try方案:余额不足!当前余额:{account.balance}")
# 22. 无论代码是否报错,最后一定会执行这里
finally:
# 23. 手动释放锁:让其他线程可以继续使用
lock1.release()
# 24. 创建第二把线程锁(和lock1功能一样,分开测试用)
lock2 = threading.Lock()
# 25. 定义【第二种安全】的取款函数:用with自动管理锁(推荐!)
def safe_draw_with(account, amount):
# 26. with lock:自动加锁,代码执行完自动解锁,不用手动操作
with lock2:
# 27. 判断余额够不够
if account.balance >= amount:
# 28. 暂停0.1秒
time.sleep(0.1)
# 29. 安全扣钱
account.balance -= amount
# 30. 打印取款成功提示
print(f"with方案:取款成功!当前余额:{account.balance}")
# 31. 余额不足时执行
else:
# 32. 打印余额不足提示
print(f"with方案:余额不足!当前余额:{account.balance}")
# ------------------- 测试代码:开始运行程序 -------------------
# 33. 程序主入口:只有直接运行这个文件时,才会执行下面的代码
if __name__ == '__main__':
# 34. 创建一个账户对象,初始余额1000元
account = Account(balance=1000)
# 35. 定义每次取款的金额:800元
amount = 800
# 36. 打印分割线:提示开始测试【不安全】取款
print("===== 测试:线程不安全(会超支)=====")
# 37. 创建第一个线程:专门执行不安全的取款方法
t1 = threading.Thread(target=unsafe_draw, args=(account, amount))
# 38. 创建第二个线程:同时执行不安全的取款方法
t2 = threading.Thread(target=unsafe_draw, args=(account, amount))
# 39. 启动第一个线程,开始取款
t1.start()
# 40. 启动第二个线程,开始取款
t2.start()
# 41. 等待第一个线程执行完,再往下走
t1.join()
# 42. 等待第二个线程执行完,再往下走
t2.join()
# 43. 打印最终的账户余额
print(f"最终账户余额:{account.balance}\n")
# 44. 重置账户余额:把钱改回1000元,继续测试安全方案
account.balance = 1000
# 45. 打印分割线:提示开始测试【try-finally安全方案】
print("===== 测试:Lock try-finally 安全方案 =====")
# 46. 创建线程1,执行第一种安全取款
t1 = threading.Thread(target=safe_draw_try, args=(account, amount))
# 47. 创建线程2,执行第一种安全取款
t2 = threading.Thread(target=safe_draw_try, args=(account, amount))
t1.start() # 启动线程1
t2.start() # 启动线程2
t1.join() # 等待线程1结束
t2.join() # 等待线程2结束
print(f"最终账户余额:{account.balance}\n") # 打印最终余额
# 48. 再次重置余额为1000元,测试第二种安全方案
account.balance = 1000
# 49. 打印分割线:提示开始测试【with自动锁安全方案】
print("===== 测试:Lock with 安全方案(推荐)=====")
# 50. 创建线程1,执行第二种安全取款
t1 = threading.Thread(target=safe_draw_with, args=(account, amount))
# 51. 创建线程2,执行第二种安全取款
t2 = threading.Thread(target=safe_draw_with, args=(account, amount))
t1.start() # 启动线程1
t2.start() # 启动线程2
t1.join() # 等待线程1结束
t2.join() # 等待线程2结束
print(f"最终账户余额:{account.balance}") # 打印最终余额
测试输出
===== 测试:线程不安全(会超支)=====
取款成功!当前余额:200
取款成功!当前余额:-600
最终账户余额:-600
===== 测试:Lock try-finally 安全方案 =====
try方案:取款成功!当前余额:200
try方案:余额不足!当前余额:200
最终账户余额:200
===== 测试:Lock with 安全方案(推荐)=====
with方案:取款成功!当前余额:200
with方案:余额不足!当前余额:200
最终账户余额:200
结论
- 线程不安全场景:未加锁时,并发操作导致余额超支($-600$)。
- Lock 效果:
- try-finally 和 with 方案均确保余额正确($200$)。
- with 模式更简洁,推荐优先使用。
通过锁机制,可保证共享变量操作的原子性,避免多线程竞争问题。实际开发中,需对所有临界区代码加锁,确保全局一致性。
更多推荐
所有评论(0)