线程同步之Lock:锁的基本原理
多线程编程里,锁这个东西,说白了就是一把「门闩」。
我刚开始接触多线程时,总觉得这玩意儿有点多余——线程跑得好好的,干嘛非要加把锁?直到有一次,我写了一个银行转账的程序,两个线程同时操作同一个账户,结果余额变成了负数。嗯,从那以后,我再也不敢小看锁了。
锁的核心思想:互斥访问
锁的作用其实很简单:同一时刻,只允许一个线程访问共享资源。
你想想看,如果多个线程同时读写同一个变量,会发生什么?数据错乱、逻辑崩溃、程序跑飞。锁就是用来解决这个问题的。
核心概念:锁本质上是一个「令牌」。线程想访问共享资源,必须先拿到这个令牌。拿不到?那就等着,直到别人用完还回来。
acquire与release:手动上锁与解锁
Python 的 threading.Lock 提供了两个最基本的方法:acquire() 和 release()。
import threading
lock = threading.Lock()
shared_data = 0
def worker():
global shared_data
for _ in range(100000):
lock.acquire() # 拿锁
shared_data += 1 # 操作共享资源
lock.release() # 放锁
threads = [threading.Thread(target=worker) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"最终结果: {shared_data}")
这段代码里,acquire() 和 release() 必须成对出现。少了一个?那就会出大问题。
⚠️ 我曾经踩过的坑:有一次我写代码,在某个分支里忘了调 release(),结果整个程序卡死了。排查了半天才发现,有个线程把锁拿走了,再也没还回来。其他线程全在等,活活等成了「死锁」。
with语句管理锁:更优雅的方式
手动 acquire 和 release 容易出错。我个人习惯用 with 语句,它会在代码块执行完后自动释放锁,省心又安全。
def worker():
global shared_data
for _ in range(100000):
with lock: # 自动 acquire 和 release
shared_data += 1
你看,代码简洁多了。而且 with 语句保证:就算代码块里抛了异常,锁也会被正确释放。这一点,手动写 try...finally 也能做到,但 with 更干净。
💡 我的建议:能用 with 就别手动调 acquire/release。除非你有特殊需求(比如超时控制),否则 with 是首选。
死锁的产生与预防
死锁,说白了就是「你等我,我等你,谁都不让」。两个线程各自拿着一把锁,又都在等对方手里的锁,结果就卡死了。
举个例子:
lock1 = threading.Lock()
lock2 = threading.Lock()
def task_a():
with lock1:
print("A 拿到了锁1")
with lock2:
print("A 拿到了锁2")
def task_b():
with lock2:
print("B 拿到了锁2")
with lock1:
print("B 拿到了锁1")
如果 task_a 先拿到 lock1,task_b 先拿到 lock2,然后 task_a 等 lock2,task_b 等 lock1——完蛋,死锁了。
死锁的四个必要条件:
- 互斥:资源一次只能被一个线程占用
- 持有并等待:线程拿着一个锁,还在等另一个锁
- 不可剥夺:别人拿不走你手里的锁
- 循环等待:形成等待环路
如何预防死锁?
我总结了几个实用方法:
- 固定锁的顺序:所有线程都按同样的顺序拿锁。比如先拿
lock1,再拿lock2。这样就不会出现循环等待。 - 使用超时机制:
acquire(timeout=1),等不到就放弃,别死等。 - 尽量少用多把锁:能用一把锁解决的问题,就别搞两把。
# 固定顺序:所有线程都先拿 lock1,再拿 lock2
def task_a():
with lock1:
with lock2:
print("A 安全执行")
def task_b():
with lock1: # 和 A 保持同样的顺序
with lock2:
print("B 安全执行")
⚠️ 我曾经遇到过一个真实案例:一个后台服务,每天凌晨都会卡死。查了三天日志,发现是某个定时任务和主业务线程拿锁的顺序不一致。改了一行代码,问题就解决了。死锁这东西,往往就藏在一行代码里。
知识体系总览
下面这张图,帮你理清本章的核心逻辑:
这张图把锁的核心知识点串起来了。从互斥访问出发,到两种使用方式(手动和 with),再到死锁的四个条件和预防方法。你把这个逻辑理清了,锁这块就算入门了。
好了,锁的基本原理就讲到这里。记住一句话:锁是工具,不是目的。用得好,它是保护数据的盾牌;用不好,它就是卡死程序的元凶。