2. 单线程 vs 多线程:线程模型对比、GIL的影响、Python多线程的适用场景
聊到任务调度,绕不开一个经典问题:单线程到底够不够用?多线程是不是万能药?
我见过不少新手,一上来就开多线程,觉得“线程越多越快”。结果呢?性能没上去,反而把系统搞崩了。今天咱们就把这事掰扯清楚。
2.1 线程模型:单线程 vs 多线程
先看一张图,这是我画的一个简单对比。你一看就明白。
说白了,单线程就像一个人干活,多线程就是叫了几个人一起干。但人多不一定快——如果大家抢同一把锤子,反而要排队等。
2.2 GIL 是什么?为什么它是个“坑”?
Python 有个东西叫 GIL(全局解释器锁)。很多初学者第一次听到它,都觉得莫名其妙。
GIL 的本质: 在 CPython 解释器中,同一时刻只能有一个线程执行 Python 字节码。
也就是说,你开了 8 个线程,但 CPU 一次只能跑一个。其他 7 个都在排队。
为什么会这样?我简单解释一下:Python 的内存管理不是线程安全的。GIL 就像一把大锁,保证一次只有一个线程操作对象。这是历史遗留设计,想改也难。
GIL 带来的影响:
- CPU 密集型任务: 多线程反而更慢。因为线程切换有开销,但计算能力没增加。
- I/O 密集型任务: 多线程有效。因为线程在等 I/O 时,会释放 GIL,让其他线程跑。
我的经验: 有一次做爬虫项目,开了 20 个线程抓数据。结果发现 CPU 占用率只有 30%。后来换成多进程,CPU 直接跑满。嗯,这就是 GIL 的“功劳”。
2.3 代码对比:单线程 vs 多线程(CPU 密集型)
咱们写个例子,看看 GIL 到底怎么影响性能。
import threading
import time
def cpu_task(n):
"""模拟 CPU 密集型计算"""
count = 0
for i in range(n):
count += i ** 2
return count
# 单线程
start = time.time()
for _ in range(4):
cpu_task(10_000_000)
print(f"单线程耗时: {time.time() - start:.2f}s")
# 多线程
start = time.time()
threads = []
for _ in range(4):
t = threading.Thread(target=cpu_task, args=(10_000_000,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"多线程耗时: {time.time() - start:.2f}s")
你猜结果怎么样?在我的机器上,单线程跑了 1.2 秒,多线程反而跑了 1.4 秒。多线程还慢了一点。这就是 GIL 的“功劳”——线程切换有开销,但计算能力没增加。
2.4 什么时候该用 Python 多线程?
既然 GIL 这么坑,那 Python 多线程是不是就没用了?当然不是。我总结了三类场景:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| I/O 密集型(网络请求、文件读写、数据库查询) | 多线程 | 线程在等待 I/O 时释放 GIL,其他线程可以执行 |
| CPU 密集型(图像处理、数值计算、加密解密) | 多进程 或 C 扩展 | 每个进程有独立 GIL,能利用多核 CPU |
| 混合型(既有计算又有 I/O) | 多线程 + 多进程 | 用多进程处理计算,多线程处理 I/O |
注意: 我曾经在一个项目中,用多线程处理大量文件解析。结果发现内存暴涨,因为每个线程都加载了完整的数据。后来改用线程池 + 流式处理,才把内存降下来。
记住:多线程不是银弹。线程数超过某个阈值后,性能反而下降。
2.5 避坑指南:我踩过的那些坑
做 Python 多线程开发,有几个坑我反复踩过。分享给你:
- 坑一:共享变量不加锁。两个线程同时修改一个列表,结果数据乱了。用
threading.Lock或者queue.Queue解决。 - 坑二:死锁。线程 A 等线程 B 释放锁,线程 B 等线程 A 释放锁。两个都卡死了。用
with lock:上下文管理器,或者设置超时时间。 - 坑三:线程数设得太大。我见过有人开 500 个线程,结果系统直接卡死。线程池大小建议用
min(32, os.cpu_count() + 4)作为起点。 - 坑四:忽略 GIL 的存在。用多线程做 CPU 密集型任务,性能反而更差。这时候该用多进程或者异步 I/O。
我的建议: 如果你不确定该用多线程还是多进程,先问自己一个问题:这个任务主要是在等 CPU 还是在等 I/O?等 I/O 就用多线程,等 CPU 就用多进程。
2.6 总结一下
单线程简单可靠,适合任务量小、不需要并发的场景。多线程能提高 I/O 密集型任务的吞吐量,但受 GIL 限制,不适合 CPU 密集型任务。
说白了,选哪种模型,取决于你的瓶颈在哪里。别盲目追求“多”,适合的才是最好的。