先看一个问题:不加锁会怎样?
假设你有一个银行账户,余额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:是你最常用的写法。