现象:没有使用lock却没有发生账户取钱bug
实例代码:
from threading import Lock
import threading
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# logger.setLevel(logging.INFO)
import time
class Account:
def __init__(self,name,money):
self.name = name
self.money = money
class GetMoney(threading.Thread):
def __init__(
self,
money:float,
account:Account):
super().__init__()
self.num = money
self.account = account
def run(self) -> float:
time.sleep(1)
if self.num > self.account.money:
logger.info(f"余额不足,当前余额为{self.account.money}")
return
self.account.money -= self.num
logger.info(f"余额为{self.account.money}")
return self.account
if __name__ == "__main__":
account = Account("cc",1000)
you = GetMoney(800,account)
your_wife = GetMoney(800,account)
you.start()
your_wife.start()
you.join()
your_wife.join()
运行结果:
INFO:__main__:余额为200
INFO:__main__:余额不足,当前余额为200
分析:
- 没有使用锁机制,所以理论上存在竞态条件(race condition),多个线程可能同时核验账户足够,导致余额变为负数
- python线程调度由操作系统和GIL共同影响,GIL确保在同一时刻仅能运行一个thread,这可能导致并发意外串行执行,但是这依赖于运气,系统负载、CPU核心数等因素并不可靠
- 竞态条件是间歇性的,取决于运行环境。生产代码中,这种问题可能在压力测试或实际部署时才暴露,导致数据不一致(如银行余额负数)。
- self.account.money -= self.num 不是原子操作:它涉及读取 money 、减法、写入。如果两个线程同时读取同一个值(例如 1000),都减 800,都写入,最终可能变成 200 而不是预期(第一个扣到 200,第二个不扣)。但没有锁,写入可能重叠,导致错误。
改进:加锁
class GetMoney(threading.Thread):
def __init__(
self,
money:float,
account:Account):
super().__init__()
self.num = money
self.account = account
def run(self) -> float:
time.sleep(1)
with Lock():
if self.num > self.account.money:
logger.info(f"余额不足,当前余额为{self.account.money}")
return
self.account.money -= self.num
logger.info(f"余额为{self.account.money}")
return self.account