一、并发基础与线程模型:进程与线程的区别、Python GIL详解、线程安全与锁机制
1.1 进程与线程:到底差在哪?
很多初学者问我,进程和线程到底有啥区别?我一般会打个比方:进程就像一家餐厅,线程就是里面的厨师。
每个餐厅(进程)都有自己的厨房、食材、菜单。厨师(线程)们共享这些资源。但餐厅之间是隔离的——A餐厅的厨师不能跑到B餐厅去炒菜。
从操作系统角度看,区别更明显:
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源开销 | 大(独立地址空间) | 小(共享地址空间) |
| 切换成本 | 高(需要切换页表) | 低(只需切换寄存器) |
| 通信方式 | IPC(管道、消息队列等) | 直接读写共享变量 |
| 健壮性 | 一个进程挂了不影响其他 | 一个线程挂了可能拖垮整个进程 |
我在项目中遇到过一个问题:用多进程处理大量IO任务,结果内存直接爆了。后来换成线程池,内存占用降了70%。选哪个,得看场景。
1.2 Python GIL:绕不开的“全局锁”
说到Python并发,GIL是个绕不开的话题。GIL全称是Global Interpreter Lock,说白了就是一把大锁,保证同一时刻只有一个线程在执行Python字节码。
为什么会这样?因为Python的内存管理不是线程安全的。CPython解释器内部用了大量全局变量,如果不加锁,多线程同时操作引用计数,内存分分钟崩掉。
核心结论:
- CPU密集型任务:多线程在Python里基本没用,甚至更慢
- IO密集型任务:多线程有效,因为GIL会在IO等待时释放
我曾经接手过一个爬虫项目,同事用多线程抓数据,但CPU占用率只有30%。我一看代码,里面做了大量加解密运算——典型的CPU密集型。改成多进程后,吞吐量直接翻了4倍。
嗯,这里要注意:GIL只在CPython中存在。如果你用Jython、IronPython或者PyPy的某些版本,是没有GIL的。但生产环境99%都是CPython,所以这个知识点必须吃透。
1.3 线程安全与锁机制
线程安全,说白了就是多个线程同时访问共享数据时,不会出现数据不一致。你想想看,如果两个线程同时给一个变量加1,结果可能只加了1次——这就是经典的“竞态条件”。
Python提供了几种锁机制:
1.3.1 Lock(互斥锁)
最基础的锁。我用一个例子说明:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # 获取锁
counter += 1
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 输出 1000000
如果不加锁,你跑十次可能得到十个不同的结果。我调试过一个线上bug,就是因为忘记加锁,导致订单金额偶尔少算了几毛钱——排查了整整两天。
1.3.2 RLock(可重入锁)
同一个线程可以多次获取RLock,不会死锁。适合递归调用或者嵌套锁的场景:
rlock = threading.RLock()
def outer():
with rlock:
inner() # 同一个线程再次获取锁,没问题
def inner():
with rlock:
pass # 如果是普通Lock,这里就死锁了
1.3.3 Semaphore(信号量)
控制同时访问资源的线程数量。我习惯用它来限制数据库连接池的并发数:
sem = threading.Semaphore(5) # 最多5个线程同时访问
def access_db():
with sem:
# 连接数据库
pass
我的经验:锁的粒度要尽量小。能锁变量就别锁函数,能锁一行就别锁十行。锁的范围越大,并发性能越差。
1.3.4 Condition(条件变量)
用于线程间的通知机制。一个线程等待某个条件,另一个线程满足条件后通知它。经典的生产者-消费者模式:
import threading
import time
buffer = []
condition = threading.Condition()
def producer():
for i in range(5):
with condition:
buffer.append(i)
print(f"生产了 {i}")
condition.notify() # 通知消费者
time.sleep(0.5)
def consumer():
while True:
with condition:
while not buffer:
condition.wait() # 等待生产者
item = buffer.pop(0)
print(f"消费了 {item}")
避坑指南:我曾经在项目里用Condition时,忘记在wait()外面套一层while循环。结果线程被虚假唤醒后,buffer还是空的,直接报IndexError。记住:wait()一定要放在while循环里检查条件。
1.4 知识体系总览
下面这张图,是我梳理的本章核心脉络:
1.5 避坑总结
最后,我把这些年踩过的坑整理一下:
- 别迷信多线程:Python的GIL决定了CPU密集场景下多线程是累赘
- 锁一定要释放:用
with lock语法,别手动acquire/release,容易忘 - 小心死锁:多个锁嵌套时,保证所有线程获取锁的顺序一致
- 虚假唤醒:Condition的wait()一定要放在while循环里
- 线程不是越多越好:线程数超过CPU核心数太多,上下文切换反而拖慢速度
我个人习惯,在写并发代码前,先画一张图理清线程之间的交互关系。画清楚了再动手,能省掉后面80%的调试时间。
想要更多资料,联系可可