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: 是你最常用的写法。

相关推荐
GuWenyue10 小时前
放弃云端API!一套React+WebGPU本地LLM方案,零数据上传、离线可用
前端·人工智能·前端框架
love530love11 小时前
OpenClaw Windows Companion 桌面客户端 连接 LM Studio 完整配置指南
人工智能·windows·python·openclaw
原则猫12 小时前
函数/变量提升
前端
独泪了无痕12 小时前
Vue3 Hooks使用实战解析
前端·vue.js
程序喵大人13 小时前
【C++进阶】STL容器与迭代器 - 01 STL 容器先解决元素放在哪里
开发语言·c++·stl
cui_ruicheng13 小时前
Python从入门到实战(十六):多进程编程
开发语言·python
shawxlee14 小时前
vue3在public下封装config.js自定义配置动态数据,可在打包后直接修改,方便后端部署及后续维护
前端·javascript·经验分享·vue·团队开发·js·项目优化
用户0595401744614 小时前
把记忆存储的回归测试从手工换成 Playwright + GitHub Actions,线上缺陷降低 80%
前端·css
触底反弹14 小时前
🚀 从 DOM0 级到 React 合成事件:前端事件监听的 20 年演进史
前端·react.js·面试
kyriewen14 小时前
我给前端项目的接口请求套了6层保护——才发现以前一直在裸奔
前端·javascript·面试