我用多线程跑 CPU 密集的计算想给程序加速,结果开了好几个线程不但没快、反而比单线程还慢,我盯着这个反常的结果查了大半天才搞懂 GIL 的深度复盘
这是一个让我对 Python "GIL"刻骨铭心的故事。我有一段 CPU 密集的计算(比如一大堆数值运算、循环处理),跑起来比较慢。我自然而然地想到:用多线程啊!我的机器有好几个 CPU 核心,开几个线程,把任务分一分,让它们并行地算,不就快了几倍吗?在我朴素的认知里,这天经地义——多线程,不就是用来"并行加速"的吗?
可结果,把我整懵了:我用多线程跑,开了 4 个线程,本以为能快接近 4 倍;结果,它不但没快,反而比单线程,还要更慢一点!我反复测,确认无误:同样的计算,单线程跑,还比多线程快。我当时百思不得其解:我明明开了多个线程、机器也明明有多个核心啊,它们怎么没并行起来?为什么多线程,反而更慢了?直到我去深究 Python 的并发模型,才恍然大悟,补上了关于 Python 并发最重要的一课:原来,(标准的 CPython)Python,有一个叫 GIL(Global Interpreter Lock,全局解释器锁)的东西!它的作用是:在任何一个时刻,只允许"一个"线程,执行 Python 的字节码。也就是说,即使我开了 4 个线程、机器有 4 个核心,这 4 个线程,也无法"真正地并行"执行 Python 代码——它们,只能轮流地、抢着那把唯一的 GIL,一个执行一会儿、就释放,换另一个执行一会儿……同一时刻,永远只有一个在真正干活。所以,对于我那个CPU 密集的计算(它需要的,正是"多个核心同时算"),多线程根本没能利用上多核,它们只是在排队轮流用那一个核心而已——速度,自然不会变快;而且,线程之间来回切换、抢 GIL,还引入了额外的开销,所以,反而比单线程,更慢了。这就解释了我所有的困惑:Python 的多线程,对 CPU 密集型任务,起不到"并行加速"的作用(因为 GIL 把它们卡成了串行);它真正能加速的,是 IO 密集型任务(因为线程在等 IO 的时候,会释放 GIL,让别的线程趁机去干活)。我把多线程,这个"在 Python 里主要用于 IO 并发"的工具,错用到了"CPU 并行计算"上——而那,正是它(因为 GIL)最无能为力的地方。
故障现场:多线程跑 CPU 密集,被 GIL 卡成串行
我把这个"多线程没加速"的现场,用代码摊开给你看:
# ✗ 灾难: 用多线程跑 CPU 密集计算, 期望并行加速
import threading
def cpu_heavy(): # CPU 密集: 纯计算, 没有 IO
total = 0
for i in range(50_000_000):
total += i * i
return total
# 开 4 个线程, 期望快 4 倍
threads = [threading.Thread(target=cpu_heavy) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
# 实测: 不但没快 4 倍, 反而比"单线程跑 4 次"还慢一点!
# 为什么? Python(CPython)有 GIL(全局解释器锁):
# - 任何时刻, 只允许"一个"线程执行 Python 字节码。
# - 4 个线程, 不能真正并行跑 Python 代码, 只能轮流抢 GIL、交替执行。
# - 同一刻只有一个在干活 → 没利用上多核 → CPU 密集不加速。
# - 还有线程切换/抢锁的开销 → 反而更慢。
# 关键区别: CPU 密集 vs IO 密集
# - CPU 密集(纯计算): 线程一直占着 CPU 算 → 一直占着 GIL →
# 多线程被 GIL 卡成串行, 不加速(本文)。
# - IO 密集(等网络/磁盘): 线程在"等 IO"时会"释放 GIL" →
# 别的线程趁机执行 → 多线程能"并发", 有加速效果。
# 所以:
# - Python 多线程: 适合 IO 密集(等的时候让出 GIL), 不适合 CPU 密集。
# - CPU 密集要并行: 用"多进程"(每个进程有自己的 GIL, 真并行)。
# 根因: GIL 让多线程无法并行执行 Python 字节码。
# CPU 密集型任务用多线程, 被 GIL 卡成串行(还加了切换开销)→ 不快反慢。
# 把"适合 IO 并发"的多线程, 错用到了"CPU 并行计算"上。
看着这段代码,我才算真正理解了这个"多线程反而更慢"的根源。问题的核心,是(标准的 CPython)Python,有一个叫 GIL(Global Interpreter Lock,全局解释器锁)的东西:它规定,在任何一个时刻,只允许"一个"线程,执行 Python 的字节码。这意味着:即使我开了 4 个线程、机器有 4 个核心,这 4 个线程,也无法真正地并行执行 Python 代码——它们,只能轮流地、抢着那把唯一的 GIL,一个执行一会儿、就释放,换另一个再执行一会儿;同一时刻,永远只有一个在真正干活。而我那个任务,恰恰是 CPU 密集的:它需要的,正是"多个核心同时算";可多线程,因为 GIL,根本没能利用上多核——4 个线程,只是在排队、轮流用那一个核心而已;速度,自然不会变快;而且,线程之间来回切换、争抢 GIL,还引入了额外的开销,所以,反而比单线程,更慢了。这里,有一个至关重要的区别,我之前完全没分清——CPU 密集 vs IO 密集:CPU 密集(纯计算):线程一直占着 CPU 算、就一直占着 GIL,于是多线程被 GIL 卡成串行,不加速(本文);IO 密集(等网络/磁盘):线程在"等 IO"的时候,会主动释放 GIL,让别的线程趁机去执行,于是多线程能"并发"、有加速效果。所以,结论就清晰了:Python 的多线程,适合 IO 密集(等的时候让出 GIL),不适合 CPU 密集;而 CPU 密集要真正并行,得用"多进程"(每个进程,有它自己独立的 GIL,所以能真并行)。归根结底:我犯的错,是把"在 Python 里主要用于 IO 并发"的多线程,错用到了"CPU 并行计算"上——而那,恰恰是它(因为 GIL)最无能为力的地方。GIL 让多线程无法并行执行 Python 字节码,于是我那个 CPU 密集任务,被卡成了串行,还白白搭上了线程切换的开销,自然不快反慢。
第一件事:搞懂 GIL——多线程不能并行执行 Python 字节码
定位到根源,我必须把"GIL 是什么、它的影响"彻底搞清楚:
GIL(全局解释器锁): CPython 里, 同一时刻只有一个线程执行字节码
# GIL 是什么?
# - CPython(标准 Python 解释器)里的一把"全局锁"。
# - 它保证: 任何时刻, 只有"一个"线程, 在执行 Python 字节码。
# - 即: 多线程, 无法"真正并行"地跑 Python 代码(只能轮流)。
# 为什么有 GIL?
# - 简化了 CPython 的内存管理(引用计数等), 让单线程更快、C 扩展更好写。
# - 代价: 牺牲了"多线程并行执行 Python 代码"的能力。
# GIL 的影响, 看任务类型:
# 1. CPU 密集(纯计算): 线程一直要 CPU、一直占 GIL。
# → 多线程被卡成串行, 不加速; 加上切换开销, 反而更慢(本文)。
# 2. IO 密集(等网络/磁盘/sleep): 线程"等"的时候会释放 GIL。
# → 别的线程趁机执行 → 多线程能并发, 有加速(这是 Python 多线程的用武之地)。
# 所以, Python 并发的"分工":
# - IO 密集 → 多线程(threading)或异步(asyncio): 等待时让出, 能并发。
# - CPU 密集 → 多进程(multiprocessing): 每个进程独立 GIL, 真正并行多核。
# - CPU 密集也可: 用 numpy 等"释放 GIL 的 C 扩展"做计算(底层不受 GIL 限制)。
# 注意:
# - GIL 是 CPython 的特性, 不是 Python 语言规范; 其它实现(如无 GIL 的方案)不同。
# - Python 3.13+ 有"实验性的无 GIL 模式", 但目前默认仍有 GIL。
# 核心: GIL 让多线程无法并行执行 Python 字节码。
# CPU 密集用多进程(真并行); IO 密集才用多线程/异步(等待时让出)。
原理终于清晰了。GIL(全局解释器锁),是 CPython(标准 Python 解释器)里的一把"全局锁";它保证:任何时刻,只有"一个"线程,在执行 Python 字节码——也就是说,多线程,无法"真正并行"地跑 Python 代码,只能轮流。为什么会有 GIL?它简化了 CPython 的内存管理(引用计数等),让单线程更快、C 扩展更好写;代价,是牺牲了"多线程并行执行 Python 代码"的能力。而 GIL 的影响,关键看任务类型:CPU 密集(纯计算):线程一直要 CPU、一直占着 GIL,于是多线程被卡成串行、不加速,加上切换开销反而更慢(本文);IO 密集(等网络/磁盘/sleep):线程"等"的时候会释放 GIL,别的线程趁机执行,于是多线程能并发、有加速(这才是 Python 多线程的用武之地)。由此,就有了 Python 并发的清晰分工:IO 密集 → 用多线程(threading)或异步(asyncio)(等待时让出,能并发);CPU 密集 → 用多进程(multiprocessing)(每个进程有独立的 GIL,能真正并行利用多核);CPU 密集也可以用 numpy 等"会释放 GIL 的 C 扩展"来做计算(底层计算不受 GIL 限制)。还有两点要注意:GIL 是 CPython 的特性,不是 Python 语言规范(其它实现不同);Python 3.13+ 有"实验性的无 GIL 模式",但目前默认仍有 GIL。归根结底:GIL 让多线程无法并行执行 Python 字节码;所以,CPU 密集要并行,用多进程(真并行);IO 密集,才用多线程/异步(等待时让出 GIL)——这,是我用一次"多线程跑 CPU 密集、不快反慢"的事故,补上的、关于 Python 并发最关键的一课。
第二件事:正解——CPU 密集用多进程,IO 密集才用多线程/异步
搞懂了根因——"GIL 让多线程跑不动 CPU 密集"——正解就清晰了:CPU 密集型任务,要并行加速,用多进程(multiprocessing)——每个进程有自己独立的 GIL,能真正并行利用多核;而 IO 密集型任务,才用多线程或异步(asyncio)(它们在等待时会让出,从而并发)。按任务类型,选对并发方式。
# 正解1: CPU 密集 → 用多进程(multiprocessing), 绕开 GIL, 真并行
from multiprocessing import Pool
def cpu_heavy(n):
total = 0
for i in range(n):
total += i * i
return total
if __name__ == "__main__":
with Pool(4) as pool: # 4 个进程, 每个独立 GIL → 真正并行多核!
results = pool.map(cpu_heavy, [50_000_000] * 4)
# ✓ 这次, 真的快了接近 4 倍(用上了 4 个核心)!
# (concurrent.futures.ProcessPoolExecutor 是更现代统一的接口)
# 正解2: IO 密集 → 用多线程 或 异步(等待时让出, 能并发)
import concurrent.futures, requests
urls = ["http://...", "http://...", ...]
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as ex:
results = list(ex.map(requests.get, urls)) # IO 密集: 多线程并发有效!
# → 等网络的时候释放 GIL, 多个请求并发进行, 快很多。
# (或用 asyncio + aiohttp, 单线程内并发更多 IO, 见 asyncio 那篇)
# 正解3: CPU 密集也可用"释放 GIL 的库"
# - numpy / pandas 等: 底层 C 实现, 大量计算时会释放 GIL, 能利用多核。
# - 把循环换成向量化的 numpy 运算, 往往既快又能并行。
# 一张"选并发方式"的对照:
# CPU 密集(纯计算) → multiprocessing(多进程) / numpy 等。
# IO 密集(等网络/盘)→ threading(多线程) / asyncio(异步)。
# 混合 → 进程 + 线程/异步 组合(进程做计算, 内部异步做 IO)。
# 注意多进程的代价:
# - 进程比线程"重": 创建开销大、占内存多、进程间通信(IPC)有成本。
# - 数据要在进程间传递(序列化), 大数据传递开销大。
# → 所以不是"无脑多进程", CPU 密集且任务够大、够独立时才划算。
# 核心: 按任务类型选并发——CPU 密集用多进程(真并行),
# IO 密集用多线程/异步(等待让出 GIL)。别再用多线程硬刚 CPU 密集。
这个正解,核心是按任务类型,选对并发的方式。正解1(CPU 密集 → 多进程):用 multiprocessing(或更现代的 ProcessPoolExecutor)——它会启动多个独立的进程,而每个进程,有它自己独立的 GIL,所以它们能真正地并行、利用上多个 CPU 核心;这样,我那个 CPU 密集的计算,真的就快了接近 4 倍。正解2(IO 密集 → 多线程/异步):对于 IO 密集型任务(如批量发网络请求),用多线程(ThreadPoolExecutor)或异步(asyncio)——它们在等待 IO 时会让出 GIL,从而让多个请求并发进行,加速明显。正解3(用释放 GIL 的库):CPU 密集也可以用 numpy/pandas 等库——它们底层是 C 实现,在大量计算时会释放 GIL、能利用多核;把 Python 循环换成向量化的 numpy 运算,往往既快、又能并行。一张"选并发方式"的对照很清晰:CPU 密集(纯计算)用 multiprocessing/numpy;IO 密集(等网络/盘)用 threading/asyncio;混合型用"进程 + 线程/异步"组合。不过,多进程也有它的代价,要注意:进程比线程"重"(创建开销大、占内存多、进程间通信 IPC 有成本);数据要在进程间传递(需序列化),大数据传递开销大;所以,不是"无脑多进程",而是在"CPU 密集、且任务够大、够独立"时,才划算。归根结底:按任务类型选并发——CPU 密集用多进程(真并行),IO 密集用多线程/异步(等待时让出 GIL);别再用多线程,去硬刚 CPU 密集型任务了。我那次的错误,正是用错了并发工具;而正解,就是给 CPU 密集的活,换上多进程这把对的工具。
下面这张图,对比了"用错"和"选对"并发方式两条路径:
这张图的对比很清楚:有个任务想并发加速,先分清它是 CPU 密集还是 IO 密集——CPU 密集用多线程是错的(被 GIL 卡成串行、不快反慢),用多进程才对(每进程独立 GIL、真并行多核、真的快几倍);IO 密集用多线程/异步(等待时让出 GIL、并发、加速)。两条路的根本分野,在于你有没有按任务类型,选对并发的方式。
第三件事:并发选型的完整考量
填平了 GIL 这个坑,我系统梳理了一遍 Python 并发选型的完整考量:
Python 并发/并行选型: 完整考量
# 三种并发模型, 对应不同场景:
# 1. 多线程(threading): 适合 IO 密集。
# - 等 IO 时释放 GIL, 多线程并发等待, 提升吞吐。
# - CPU 密集无效(GIL)。线程切换有开销, 别开太多。
# 2. 异步(asyncio): 适合"大量 IO 并发"(高并发网络)。
# - 单线程内, 用事件循环并发处理海量 IO(协程), 比多线程更省资源。
# - 但要"全链路异步"(见 asyncio 那篇), 且 CPU 密集会阻塞事件循环。
# 3. 多进程(multiprocessing): 适合 CPU 密集。
# - 每进程独立 GIL, 真正并行多核。
# - 进程重、IPC 有成本; 适合"大块、独立"的计算任务。
# 选型决策:
# - IO 密集 + 并发量中等 → 多线程。
# - IO 密集 + 高并发(成千上万) → asyncio。
# - CPU 密集 → 多进程(或 numpy 等释放 GIL 的库)。
# - 混合(既算又 IO)→ 多进程 + 进程内异步/线程。
# 其它要点:
# - 控制并发度(线程池/进程池大小), 别无限开。
# - 共享状态要保护(锁/队列), 多进程间用 Queue/Pipe/共享内存通信。
# - CPU 密集的进程数, 一般 ≈ CPU 核数(再多也没核可用)。
# 一个常见误区(本文): "并发 = 多线程", 然后无脑多线程。
# → 实际: 并发有三种模型, 要按"CPU 密集 / IO 密集"和并发量来选。
# 核心: 没有万能的并发方式。CPU 密集多进程、IO 密集多线程/异步;
# 按任务类型和并发量选对工具, 才能真正加速。
这一梳理,让我对 Python 并发选型,有了体系化的认识。三种并发模型,对应不同场景:多线程(threading)——适合 IO 密集(等 IO 时释放 GIL、多线程并发等待、提升吞吐;CPU 密集无效;线程切换有开销、别开太多);异步(asyncio)——适合"大量 IO 并发"(单线程内用事件循环并发处理海量 IO 协程,比多线程更省资源;但要全链路异步,且 CPU 密集会阻塞事件循环);多进程(multiprocessing)——适合 CPU 密集(每进程独立 GIL、真正并行多核;但进程重、IPC 有成本,适合大块、独立的计算)。而选型决策:IO 密集 + 并发量中等 → 多线程;IO 密集 + 高并发(成千上万)→ asyncio;CPU 密集 → 多进程(或 numpy);混合型 → 多进程 + 进程内异步/线程。还有几个要点:控制并发度(线程池/进程池的大小,别无限开)、共享状态要保护(锁/队列,多进程间用 Queue/Pipe/共享内存通信)、CPU 密集的进程数一般约等于 CPU 核数(再多也没核可用)。而我犯的,正是一个常见的误区:"并发 = 多线程",然后无脑地用多线程;可实际上,并发有三种模型,要按"CPU 密集 / IO 密集"和并发量,来选对。归根结底:没有万能的并发方式;CPU 密集用多进程、IO 密集用多线程/异步;按任务类型和并发量,选对工具,才能真正加速。把这套选型刻在心里,就不会再像我那样,拿着多线程这把"锤子",去拧 CPU 密集这颗"螺丝"了。
第四件事:分清 CPU 密集和 IO 密集
这次踩坑,逼我把"CPU 密集"和"IO 密集"这两个概念,以及怎么判断,彻底搞清楚了——这是选对并发方式的前提:
CPU 密集 vs IO 密集: 怎么分, 为什么决定并发选型
# CPU 密集(CPU-bound): 瓶颈在"算"
# - 大部分时间, CPU 在忙着计算。
# - 例: 数值计算、加解密、压缩、图像/视频处理、复杂循环、序列化大对象。
# - 特征: 跑的时候 CPU 占用很高(接近 100%), 没在等什么。
# IO 密集(IO-bound): 瓶颈在"等"
# - 大部分时间, 在"等待"外部(网络、磁盘、数据库)的响应。
# - 例: 调接口、查数据库、读写文件、爬虫、消息收发。
# - 特征: 跑的时候 CPU 占用低, 大量时间在"等 IO"。
# 为什么这个区分决定并发选型?
# - CPU 密集: 瓶颈是"算力"→ 要更多核同时算 → 多进程(绕开 GIL)。
# (多线程没用: GIL 让它们抢一个核; 线程数多也没意义)
# - IO 密集: 瓶颈是"等待"→ 等的时候让 CPU 去干别的 → 多线程/异步。
# (等 IO 时释放 GIL/让出协程, 一个线程能"管"很多个等待中的 IO)
# 怎么判断你的任务是哪种?
# - 看它跑的时候: CPU 占用高(满核)→ CPU 密集; CPU 闲、在等 → IO 密集。
# - 看它在干嘛: 纯算 → CPU 密集; 调接口/查库/读文件 → IO 密集。
# - 不确定就 profile(性能分析), 看时间花在"算"还是"等"上。
# 混合型: 既有计算又有 IO → 拆开, 各用各的(IO 部分异步, 计算部分多进程)。
# 核心: 先分清 CPU 密集(瓶颈在算)还是 IO 密集(瓶颈在等),
# 再选并发方式——这个区分, 是 Python 并发选型的第一步, 也是最关键的一步。
这一梳理,让我搞清了选对并发方式的前提——分清任务是 CPU 密集还是 IO 密集。CPU 密集(CPU-bound):瓶颈在"算"——大部分时间,CPU 在忙着计算(如数值计算、加解密、压缩、图像处理、复杂循环、序列化大对象);特征是,跑的时候 CPU 占用很高(接近 100%),没在等什么。IO 密集(IO-bound):瓶颈在"等"——大部分时间,在等待外部(网络、磁盘、数据库)的响应(如调接口、查数据库、读写文件、爬虫);特征是,跑的时候 CPU 占用低,大量时间在"等 IO"。而这个区分,为什么决定了并发选型?因为:CPU 密集的瓶颈是"算力",要靠"更多核同时算"来加速,所以用多进程(绕开 GIL);多线程对它没用(GIL 让线程们抢一个核)。IO 密集的瓶颈是"等待",可以在"等的时候,让 CPU 去干别的",所以用多线程/异步(等 IO 时释放 GIL/让出协程,一个线程就能"管"很多个等待中的 IO)。那怎么判断你的任务是哪种?看它跑时的 CPU 占用(满核 → CPU 密集;CPU 闲、在等 → IO 密集);看它在干嘛(纯算 → CPU 密集;调接口/查库/读文件 → IO 密集);不确定就 profile(性能分析),看时间到底花在"算"还是"等"上。而混合型(既有计算又有 IO),就拆开、各用各的(IO 部分异步、计算部分多进程)。归根结底:先分清 CPU 密集(瓶颈在算)还是 IO 密集(瓶颈在等),再选并发方式——这个区分,是 Python 并发选型的第一步,也是最关键的一步。我那次,正是没分清,把 CPU 密集的任务,当成了能用多线程加速的任务。把两者的区别,整理成一张表:
| 维度 | CPU 密集 | IO 密集 |
|---|---|---|
| 瓶颈 | 算力(一直在算) | 等待(一直在等外部) |
| CPU 占用 | 高(接近满核) | 低(大量时间等) |
| 典型任务 | 计算/加解密/图像处理 | 调接口/查库/读写文件 |
| 该用 | 多进程(绕开 GIL) | 多线程/异步(等时让出) |
| 多线程效果 | 无效(被 GIL 卡住) | 有效(并发等待) |
第五件事:理解工具的"适用场景",别拿锤子拧螺丝
这次踩坑,在认知层面给了我最大的纠偏——它让我警惕"把工具用在它不擅长的场景"。我把这层反思,沉淀了下来:
认知纠偏: 每个工具有它的"适用场景", 别拿锤子拧螺丝
# 我的误解(错误的):
# 我以为"多线程 = 并行加速"的万能工具, 不管什么任务都拿它来加速。
# → 我没理解"多线程在 Python 里到底擅长什么、不擅长什么"。
# 真相: 每个工具/技术, 都有它"擅长"和"不擅长"的场景
# - Python 多线程: 擅长 IO 密集(等待时让出 GIL), 不擅长 CPU 密集(GIL)。
# - 我把它用在了它"最不擅长"的 CPU 密集上 → 不但没效果, 反而更糟。
# - "拿着锤子, 看什么都像钉子"——把一个工具, 当成万能的, 是常见的错。
# 这是一个普遍的工程智慧: "为问题, 选对工具"
# - 没有"万能"的技术; 每个技术, 是为"某类问题"设计的, 有它的边界。
# - 用对了场景, 事半功倍; 用错了场景(它不擅长的), 事倍功半甚至帮倒忙。
# - 例: 多线程 vs 多进程 vs 异步、SQL vs NoSQL、缓存 vs 不缓存……
# 都要看"问题的特征", 选"匹配的工具", 而非"我熟的/听起来酷的"。
# 正确的习惯:
# 1. 用一个工具前, 搞清它"擅长解决什么问题、有什么前提和局限"。
# 2. 先理解"问题的特征"(如 CPU 密集还是 IO 密集), 再选匹配的工具。
# 3. 别迷信某个工具是"万能"的——它再好, 也有它不适用的场景。
# 核心: 每个工具有它的适用场景。理解它"擅长/不擅长"什么,
# 为问题选对工具——别拿"多线程"这把锤子, 去拧"CPU 密集"这颗螺丝。
这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我以为"多线程 = 并行加速"的万能工具,不管什么任务,都拿它来加速;我没理解"多线程在 Python 里,到底擅长什么、不擅长什么"。可真相是:每个工具/技术,都有它"擅长"和"不擅长"的场景——Python 的多线程,擅长 IO 密集(等待时让出 GIL),不擅长 CPU 密集(被 GIL 卡住);而我,恰恰把它用在了它"最不擅长"的 CPU 密集上,所以不但没效果,反而更糟。这,就是那句老话——"拿着锤子,看什么都像钉子":把一个工具,当成万能的,是一个常见的错。而这,是一个普遍的工程智慧——"为问题,选对工具":没有"万能"的技术;每个技术,都是为"某一类问题"设计的,有它的边界;用对了场景,事半功倍;用错了场景(它不擅长的),事倍功半、甚至帮倒忙。比如:多线程 vs 多进程 vs 异步、SQL vs NoSQL、缓存 vs 不缓存……都要看"问题的特征",选"匹配的工具",而不是选"我熟的、或听起来酷的"。由此,我立下了几条习惯:第一,用一个工具之前,搞清它"擅长解决什么问题、有什么前提和局限";第二,先理解"问题的特征"(比如,是 CPU 密集还是 IO 密集),再选匹配的工具;第三,别迷信某个工具是"万能"的——它再好,也有它不适用的场景。归根结底:每个工具,都有它的适用场景;理解它"擅长/不擅长"什么,为问题选对工具——别拿"多线程"这把锤子,去拧"CPU 密集"这颗螺丝。我那次的不快反慢,正是一次典型的"锤子拧螺丝"。把"工具当万能"和"为问题选工具"对比成一张表:
| 维度 | 工具当万能(踩坑) | 为问题选工具(成熟) |
|---|---|---|
| 用多线程 | 什么都拿它加速 | 只用于 IO 密集 |
| 出发点 | 我熟/听起来酷 | 问题的特征 |
| 对工具 | 以为万能 | 清楚它擅长/不擅长 |
| 用错场景时 | 事倍功半/帮倒忙 | 提前避开 |
| 选型依据 | 拿着锤子找钉子 | 为螺丝找螺丝刀 |
一套"Python 并发该选什么"的决策流程
把这次踩坑的全部教训,我浓缩成了一张"Python 里要并发加速、该选什么"的决策图,贴在了团队的规范里:
这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:要并发加速,第一步永远是判断 CPU 密集还是 IO 密集(瓶颈在算还是在等)——CPU 密集(在算)用多进程(或 numpy 等释放 GIL 的库);IO 密集(在等)看并发量:中等用多线程、高并发用异步;混合型就"进程做计算 + 进程内异步/线程做 IO"。最后,控制并发度、别无限开。这条"先判断任务类型、再按瓶颈和并发量选并发模型"的决策链,现在是我们团队做每一个并发优化时的准则。
我立下的几条 Python 并发规矩
这次"多线程跑 CPU 密集不快反慢"的踩坑,让我把 Python 并发的注意事项,认真地立成了几条规矩:
- 记牢 GIL:多线程不能并行执行 Python 字节码。同一时刻只有一个线程跑 Python 代码。
- CPU 密集用多进程。
multiprocessing/ProcessPoolExecutor,每进程独立 GIL,真并行多核。 - IO 密集才用多线程/异步。等待时让出 GIL,能并发;高并发用 asyncio 更省资源。
- 先分清 CPU 密集还是 IO 密集。看瓶颈在"算"还是"等",这是选并发方式的第一步。
- CPU 密集也可用 numpy 等。底层 C 实现、计算时释放 GIL,向量化往往又快又能并行。
- 控制并发度、注意多进程代价。进程数约等于核数;进程重、IPC/序列化有成本,任务够大才划算。
- 别把工具当万能。为问题选对工具,理解它擅长/不擅长什么,别拿锤子拧螺丝。
写在最后
这次"我用多线程跑 CPU 密集、结果不快反慢"的经历,是我在 Python 路上,一次很经典、也很受用的成长。它教给我的,远不止"CPU 密集用多进程"这一条具体的技术经验,更是一个普适的工程智慧——每个工具,都有它的适用场景;为问题,选对工具,别拿锤子拧螺丝。我那个不快反慢的多线程,根源就在于,我把"多线程"当成了"并行加速"的万能工具,却不知道,在 Python 里,因为 GIL,它真正擅长的,是 IO 密集的并发等待,而对 CPU 密集的并行计算,恰恰最无能为力——我把它,用在了它最不擅长的地方。
所以,当你要用某个工具、某项技术去解决问题时,请别想当然地以为它"万能",而要先搞清楚两件事:一是,这个工具,到底擅长解决什么问题、有什么前提和局限;二是,我手上这个问题,它的特征是什么(比如,是 CPU 密集还是 IO 密集);然后,为这个问题,选一个真正匹配的工具。就像 Python 的并发,你只要先分清任务是"瓶颈在算"还是"瓶颈在等",就知道该用多进程、还是多线程/异步,绝不会再经历那种"开了多线程、却不快反慢"的反常。从"把工具当万能"到"为问题选对工具",从"并发就是多线程"到"按 CPU/IO 密集选并发模型",是从一个"会用并发 API"的开发,走向一个"懂原理、能选对工具"的工程师,必经的修炼。愿你解决的每一个问题,手里握着的,都是那把最趁手的工具;也愿你我,永远先看清问题的样子,再去挑工具——而不是拿着一把锤子,把所有问题,都看成钉子。共勉。
—— 别看了 · 2026