线程同步机制:从一把锁说起

多线程编程里,最让人头疼的问题是什么?

我个人觉得,就是「数据打架」。两个线程同时修改同一个变量,结果谁也不知道最终值是多少。我早年做金融交易系统时,就因为这个问题,差点让一笔订单重复执行了两次。嗯,从那以后,我对线程同步就格外上心。

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. 固定锁的顺序:所有线程都按同样的顺序获取锁。比如先拿锁1再拿锁2,就不会出现循环等待。
  2. 使用超时机制lock.acquire(timeout=1),拿不到就放弃,别死等。
  3. 尽量少用多把锁:能用一把锁解决的问题,别用两把。
避坑指南: 我曾经在一个支付系统里,用了三把锁来保护不同的账户余额。结果某次异常流程下,三个线程互相等待,整个支付模块挂了5分钟。后来我改成一把全局锁 + 细粒度的事务日志,问题彻底解决。记住:锁越少,死锁概率越低。

三种锁的对比

类型 特点 适用场景 风险
Lock 最基础,不可重入 简单互斥场景 同一线程重复acquire会死锁
RLock 可重入,同一线程可多次acquire 递归函数、嵌套调用 release次数必须匹配
死锁 不是锁类型,是错误状态 多锁交叉等待时发生 程序永久阻塞

最后说一句:线程同步没有银弹。Lock和RLock是工具,死锁是陷阱。你用得越多,就越能体会到「少即是多」的道理。我做了十年并发编程,现在写多线程代码,第一反应永远是——能不能不用锁?

嗯,这就是我今天想分享的内容。记住:锁是保护数据的,不是用来折磨自己的。