2. 单线程 vs 多线程:线程模型对比、GIL的影响、Python多线程的适用场景

聊到任务调度,绕不开一个经典问题:单线程到底够不够用?多线程是不是万能药?

我见过不少新手,一上来就开多线程,觉得“线程越多越快”。结果呢?性能没上去,反而把系统搞崩了。今天咱们就把这事掰扯清楚。

2.1 线程模型:单线程 vs 多线程

先看一张图,这是我画的一个简单对比。你一看就明白。

单线程模型 任务A → 任务B → 任务C 特点:顺序执行,一个做完才做下一个 多线程模型 线程1: 任务A → 任务D 线程2: 任务B → 任务E 线程3: 任务C → 任务F 核心区别 • 单线程:资源占用少,没有竞态问题,但CPU利用率低 • 多线程:能利用多核CPU,但需要处理锁、死锁、上下文切换开销 • 关键指标:不是线程越多越好,线程数 ≈ CPU核心数 × (1 + 等待时间/计算时间)

说白了,单线程就像一个人干活,多线程就是叫了几个人一起干。但人多不一定快——如果大家抢同一把锤子,反而要排队等。

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 密集型任务。

说白了,选哪种模型,取决于你的瓶颈在哪里。别盲目追求“多”,适合的才是最好的。

可可精品资料库