线程同步机制:从一把锁说起
多线程编程里,最让人头疼的问题是什么?
我个人觉得,就是「数据打架」。两个线程同时修改同一个变量,结果谁也不知道最终值是多少。我早年做金融交易系统时,就因为这个问题,差点让一笔订单重复执行了两次。嗯,从那以后,我对线程同步就格外上心。
Lock锁:最基础的同步工具
说白了,Lock锁就像厕所门上的插销。一个人进去了,把门锁上,其他人就得排队等着。等里面的人出来,下一个人才能进。
在Python里,用起来特别简单:
import threading
# 创建一把锁
lock = threading.Lock()
shared_counter = 0
def increment():
global shared_counter
for _ in range(100000):
lock.acquire() # 上锁
shared_counter += 1
lock.release() # 解锁
# 启动两个线程
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"最终结果: {shared_counter}")
# 输出: 200000 ✅ 正确
你想想看,如果没有这把锁,两个线程同时执行 shared_counter += 1,可能会发生什么?
线程A读取到100,线程B也读取到100。然后各自加1,都写回101。明明加了两次,结果只增加了1。这就是经典的「竞态条件」。
核心要点: Lock锁保证同一时刻只有一个线程能访问共享资源。但要注意,
acquire() 和 release() 必须成对出现,少一个就会死锁。
RLock可重入锁:同一个线程能重复上锁
有一次我在项目中写了一个递归函数,里面需要加锁。用普通的Lock,结果程序直接卡死了。为什么?
因为同一个线程已经持有了锁,再次 acquire() 时,自己把自己堵在门外了。这就像你进了厕所,插上门,然后想再插一次——门已经锁了,你出不去了。
RLock就是解决这个问题的。它允许同一个线程多次 acquire(),只要对应的 release() 次数匹配就行。
import threading
rlock = threading.RLock()
def recursive_function(n):
rlock.acquire()
try:
print(f"深度: {n}")
if n > 0:
recursive_function(n - 1) # 同一线程再次上锁 ✅ 没问题
finally:
rlock.release()
recursive_function(3)
# 输出:
# 深度: 3
# 深度: 2
# 深度: 1
# 深度: 0
我的习惯: 只要不确定会不会在同一个线程里重复加锁,我就直接用RLock。性能损失微乎其微,但能避免很多隐蔽的bug。
死锁:最让人头疼的问题
死锁是什么?我打个比方:
线程A拿着锁1,等锁2。线程B拿着锁2,等锁1。两个人互相等对方放手,结果谁都不放,程序就永远卡在那了。
我在项目中遇到过最经典的一个死锁场景:
import threading
lock1 = threading.Lock()
lock2 = threading.Lock()
def worker1():
lock1.acquire()
print("worker1 拿到锁1")
import time; time.sleep(0.1)
lock2.acquire() # 等锁2,但锁2被worker2拿着
print("worker1 拿到锁2")
lock2.release()
lock1.release()
def worker2():
lock2.acquire()
print("worker2 拿到锁2")
import time; time.sleep(0.1)
lock1.acquire() # 等锁1,但锁1被worker1拿着
print("worker2 拿到锁1")
lock1.release()
lock2.release()
t1 = threading.Thread(target=worker1)
t2 = threading.Thread(target=worker2)
t1.start()
t2.start()
t1.join()
t2.join()
# 程序卡死在这里... 😱
为什么会这样?两个线程各自拿到一把锁,然后都在等对方释放。谁也不让谁,死锁就产生了。
如何避免死锁?
我总结了三条实战经验:
- 固定锁的顺序:所有线程都按同样的顺序获取锁。比如先拿锁1再拿锁2,就不会出现循环等待。
- 使用超时机制:
lock.acquire(timeout=1),拿不到就放弃,别死等。 - 尽量少用多把锁:能用一把锁解决的问题,别用两把。
避坑指南: 我曾经在一个支付系统里,用了三把锁来保护不同的账户余额。结果某次异常流程下,三个线程互相等待,整个支付模块挂了5分钟。后来我改成一把全局锁 + 细粒度的事务日志,问题彻底解决。记住:锁越少,死锁概率越低。
三种锁的对比
| 类型 | 特点 | 适用场景 | 风险 |
|---|---|---|---|
| Lock | 最基础,不可重入 | 简单互斥场景 | 同一线程重复acquire会死锁 |
| RLock | 可重入,同一线程可多次acquire | 递归函数、嵌套调用 | release次数必须匹配 |
| 死锁 | 不是锁类型,是错误状态 | 多锁交叉等待时发生 | 程序永久阻塞 |
最后说一句:线程同步没有银弹。Lock和RLock是工具,死锁是陷阱。你用得越多,就越能体会到「少即是多」的道理。我做了十年并发编程,现在写多线程代码,第一反应永远是——能不能不用锁?
嗯,这就是我今天想分享的内容。记住:锁是保护数据的,不是用来折磨自己的。