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

相关推荐
程序员雷欧41 分钟前
Java 反射深度解析:从原理到源码的全面剖析
java·开发语言·python
登登登__1 小时前
春秋招笔试题总结
java·开发语言
大家的林语冰1 小时前
👍 超越 ESLint,Oxc 优先采用 TypeScript 7,Rust 和 Go 梦幻联动!
前端·javascript·typescript
我的xiaodoujiao1 小时前
快速学习Python基础知识详细图文教程18--多线程
开发语言·python·学习·测试工具
想要入门的程序猿1 小时前
Qt插件QPluginLoader
开发语言·qt
Rabitebla2 小时前
C++11 新特性详解(一):列表初始化、initializer_list 与右值引用
java·开发语言·数据结构·c++·算法·leetcode·list
洋不写bug2 小时前
JavaComparable、Comparator、Cloneable接口解析,深拷贝浅拷贝详细解析
android·java·开发语言
吴声子夜歌2 小时前
正则指引——Golang
开发语言·golang·正则表达式
坐吃山猪2 小时前
WebFlux_学习Subscriber总结
java·开发语言·学习·webflux
白露与泡影2 小时前
Java面试题及答案整理(2026年金九银十最新版,持续更新)
java·开发语言