Python线程锁:为什么多线程会“打架“,以及怎么解决

先看一个问题:不加锁会怎样?

假设你有一个银行账户,余额1000元。两个线程同时取钱:

python 复制代码
import threading

balance = 1000

def withdraw(amount):
    global balance
    temp = balance          # 读余额
    temp -= amount          # 减钱
    balance = temp          # 写回余额
    print(f"取出 {amount}, 剩余 {balance}")

t1 = threading.Thread(target=withdraw, args=(800,))
t2 = threading.Thread(target=withdraw, args=(800,))

t1.start()
t2.start()
t1.join()
t2.join()

print(f"最终余额: {balance}")

你觉得最终余额是多少?

答案:可能是 -600,也可能是 200,也可能是 400。

因为两个线程同时在读写 balance,互相覆盖了对方的结果。这就是竞态条件(Race Condition)。


用一张图理解发生了什么

时间 线程1 线程2 balance
T1 读 balance = 1000 1000
T2 读 balance = 1000 1000
T3 减800 → temp = 200 1000
T4 减800 → temp = 200 1000
T5 写回 balance = 200 200
T6 写回 balance = 200 200

两个线程各取了800,总共取了1600,但余额只少了800。钱"丢"了。


线程锁是什么?

一句话:锁就是一把钥匙,同一时间只有一个线程能拿到钥匙,进门干活。

python 复制代码
import threading

balance = 1000
lock = threading.Lock()  # 创建一把锁

def withdraw(amount):
    global balance
    lock.acquire()         # 🔑 拿钥匙,进门
    try:
        temp = balance
        temp -= amount
        balance = temp
        print(f"取出 {amount}, 剩余 {balance}")
    finally:
        lock.release()     # 🔓 还钥匙,出门

t1 = threading.Thread(target=withdraw, args=(800,))
t2 = threading.Thread(target=withdraw, args=(800,))

t1.start()
t2.start()
t1.join()
t2.join()

print(f"最终余额: {balance}")

输出:

复制代码
取出 800, 剩余 200
取出 800, 剩余 -600
最终余额: -600

✅ 结果正确了。虽然余额是负数(业务逻辑问题),但两次取钱都被正确记录了,没有互相覆盖。


更推荐的写法:with 语句

手动 acquire/release 容易忘,用 with 自动管理:

python 复制代码
def withdraw(amount):
    global balance
    with lock:              # 自动拿钥匙 + 自动还钥匙
        temp = balance
        temp -= amount
        balance = temp

✅ 推荐所有场景都用 with lock:,不会忘还钥匙。


锁的三个核心问题

问题 说明 解决方案
什么时候加锁? 只要多个线程会同时读写同一个变量,就要加锁 共享数据必加锁
锁的范围多大? 锁住的代码越少越好,只锁读写那几行 不要把整个函数都锁住
忘记释放锁? 线程崩溃,锁没还,其他线程永远等着(死锁) 用 with lock: 自动释放

实战:你之前的响应时间列表

回到你之前的场景:

python 复制代码
from collections import deque
import threading

response_times = deque(maxlen=100)
lock = threading.Lock()

def add_response_time(t):
    with lock:              # 加锁保护
        response_times.append(t)

def get_average():
    with lock:
        if len(response_times) == 0:
            return 0
        return sum(response_times) / len(response_times)

✅ 任何时候操作 response_times,都先拿锁,避免多线程同时修改导致数据错乱。


什么时候不需要锁?

场景 是否需要锁 原因
多个线程只读,不写 ❌ 不需要 读不会互相影响
每个线程操作自己的变量 ❌ 不需要 没有共享数据
多个线程读写同一个变量 ✅ 必须加锁 会互相覆盖

一句话总结

线程锁 = 排队机制。多个线程抢同一个资源时,锁保证同一时间只有一个线程能操作,防止数据被互相踩踏。

记住:with lock: 是你最常用的写法。

相关推荐
陈随易2 小时前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
belldeep4 小时前
python:genesis-world, Genesis-Embodied-AI
人工智能·python·ai·genesis
布莱克6054 小时前
C/C++ 中变量的声明与定义:区别、联系与常见误区
c语言·开发语言·c++·声明·定义
LabVIEW开发4 小时前
LabVIEW 局部变量为何引发竞争条件?四种同步机制
开发语言·labview
benchmark_cc4 小时前
A 股、港股、美股如何建立统一的标的主数据格式?
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
维克兜率天5 小时前
【维克】趋势型CTA:期货市场的“追涨杀跌“
人工智能·python
Csvn5 小时前
性能监控
前端
Zenova EdgeOS6 小时前
Go 工业边缘错误处理实战:用 errors 构建可追溯错误链
开发语言·数据库·golang
Csvn6 小时前
构建工具
前端
取名真是6 小时前
Python 算法知识点从基础到进阶
开发语言·python·算法