我用 Python 多线程并行跑 CPU 密集计算想提速,结果开了 8 个线程比单线程还慢,我对着 GIL 排查了大半天的复盘
那是我写的一个 CPU 密集型的计算任务:一大批数据,每条都要做一堆纯计算(数学运算、循环处理)。我想当然地用了多线程来"并行加速":开 8 个线程,以为能快 8 倍。结果跑出来的数据让我目瞪口呆:开了 8 个线程,不但没快 8 倍,反而比单线程还慢了一点。我盯着 CPU 监控更困惑了:我机器明明是 8 核,可跑的时候,8 个线程加起来只用满了大约 1 个核的算力,其他 7 个核基本闲着!多线程不是为了利用多核并行吗?怎么 8 个线程挤在 1 个核上跑、还互相拖累?排查了大半天,我才真正理解了 Python 那个又爱又恨、绕不开的设计:GIL(全局解释器锁)。这篇就把这场"多线程不快反慢"的事故,从头复盘一遍。
故障现场:8 线程比单线程还慢,8 核只用满 1 核
先看现场。多线程在 CPU 密集任务上,完全没起到并行加速的效果:
import threading, time
# CPU 密集型任务: 纯计算(没有 IO)
def cpu_heavy(n):
total = 0
for i in range(n):
total += i * i # 纯 CPU 计算
return total
N = 50_000_000
# 单线程: 串行跑 8 次
start = time.time()
for _ in range(8):
cpu_heavy(N)
print(f"单线程: {time.time() - start:.2f}s") # 比如 ~8s
# 多线程: 8个线程"并行"跑 (我以为会快很多)
start = time.time()
threads = [threading.Thread(target=cpu_heavy, args=(N,)) for _ in range(8)]
for t in threads: t.start()
for t in threads: t.join()
print(f"8线程: {time.time() - start:.2f}s") # ✗ 比如 ~8.5s, 比单线程还慢!
# 现象: 8核机器, 但 8 个线程加起来只用满约 1 个核(其他核闲着)!
# 为什么? 因为 Python 的 GIL(全局解释器锁):
# 1. GIL = Global Interpreter Lock, 是 CPython 解释器的一把"全局锁"。
# 2. 它规定: 【同一时刻, 只有一个线程能执行 Python 字节码】。
# → 即使你开了 8 个线程、机器有 8 个核, 这 8 个线程也【不能真正同时】
# 执行 Python 代码! 它们要【轮流】抢 GIL, 一次只有一个在跑。
# 3. 所以对 CPU 密集任务: 8个线程 ≈ 还是串行(轮流执行), 没有并行加速!
# → 8个线程的计算量, 还是挤在"1个核的算力"上轮流做。
# 4. 而且: 线程还要不断"抢GIL、释放GIL、切换", 这个开销让它比单线程还慢!
# 现象拼图:
# - GIL 让"同一时刻只有一个线程执行Python字节码" → 多线程无法并行算CPU。
# - CPU密集任务: 多线程 = 串行 + 额外的GIL切换开销 → 不快反慢。
# - 8核只用满1核, 正是因为同时只有1个线程在执行Python代码。
# - ★ 根因: 我用了"多线程"去加速"CPU密集"任务, 但 GIL 决定了
# Python多线程对CPU密集任务无效。我用错了并发模型。
看清真相后,我才明白这"不快反慢"的根子。问题的根源,是 Python 的 GIL(全局解释器锁)。GIL 是 CPython 解释器的一把"全局锁",它规定:同一时刻只有一个线程能执行 Python 字节码。所以,即使我开了 8 个线程、机器有 8 个核,这 8 个线程也不能真正同时执行 Python 代码——它们要轮流抢 GIL,一次只有一个在跑。对 CPU 密集任务而言:8 个线程 ≈ 还是串行(轮流执行),没有并行加速,8 个线程的计算量还是挤在"1 个核的算力"上轮流做(所以 8 核只用满 1 核);而且线程还要不断抢 GIL、释放、切换,这个额外开销让它比单线程还慢。根因是:我用了"多线程"去加速"CPU 密集"任务,但 GIL 决定了 Python 多线程对 CPU 密集任务无效——我用错了并发模型。
第一件事:搞懂 GIL 是什么、它影响什么
要解决它,得先彻底搞懂 GIL,以及它对不同类型任务的不同影响。
GIL(全局解释器锁)详解
# 一、GIL 是什么?
# - CPython(最主流的Python解释器)里的一把"全局互斥锁"。
# - 规定: 同一时刻, 只有一个线程能持有GIL、执行Python字节码。
# - 即: Python的多线程, 无法在多核上"真正并行地执行Python代码"。
# 二、为什么有GIL?(历史/设计原因)
# - CPython的内存管理(引用计数)不是线程安全的。
# - GIL用"一把大锁"简单粗暴地保证了线程安全(同时只有一个线程动内存)。
# - 代价: 牺牲了多线程的并行计算能力, 换取了实现的简单和单线程的高效。
# 三、关键: GIL对"CPU密集"和"IO密集"任务的影响【完全不同】!
# - CPU密集任务(纯计算: 数学/循环/图像处理):
# * 线程大部分时间在"执行Python字节码"、占着GIL。
# * 多线程也只能轮流执行 → 无法并行 → 多线程【无效甚至更慢】。
# - IO密集任务(网络请求/文件读写/数据库查询):
# * 线程在等IO时(如等网络响应), 会【释放GIL】!
# * 这时其他线程可以拿到GIL去执行 → 实现了"并发"(不是并行, 但有效)。
# * 所以多线程对IO密集任务【是有效的】(等IO时切换去干别的)。
# 四、所以并发模型的选择, 取决于任务类型:
# - CPU密集 → 用【多进程】(每个进程有自己的GIL和解释器, 真正并行多核)。
# - IO密集 → 用【多线程】或【异步asyncio】(等IO时切换, GIL不是瓶颈)。
# - 混合 → 按主要瓶颈选, 或组合使用。
# 五、注意: GIL 是 CPython 的特性, 不是Python语言规范
# - 其他实现(Jython/IronPython)没有GIL; PyPy有。
# - Python 3.13 开始有"无GIL"的实验性版本(free-threaded), 未来可能改变。
# 核心: GIL是CPython的全局锁, 同时只一个线程执行Python字节码、多线程无法并行算CPU;
# CPU密集任务多线程无效(用多进程), IO密集任务等IO释放GIL故多线程有效(或用asyncio)。
想透 GIL,这个诡异的现象就清楚了。一、GIL 是什么?——CPython 里的一把全局互斥锁,规定同一时刻只有一个线程能持有它、执行 Python 字节码,即 Python 多线程无法在多核上真正并行执行 Python 代码。二、为什么有 GIL?——CPython 的内存管理(引用计数)不是线程安全的,GIL 用一把大锁简单保证了线程安全,代价是牺牲了多线程的并行计算能力。三、关键:GIL 对"CPU 密集"和"IO 密集"任务的影响完全不同!——CPU 密集任务(纯计算)线程大部分时间占着 GIL、多线程只能轮流执行、无法并行(无效甚至更慢);IO 密集任务(网络/文件/数据库)线程在等 IO 时会释放 GIL、其他线程可以拿到 GIL 执行、实现了有效的"并发",所以多线程对 IO 密集任务是有效的。四、并发模型的选择取决于任务类型:CPU 密集用多进程(每个进程有自己的 GIL,真正并行多核)、IO 密集用多线程或异步 asyncio。五、注意:GIL 是 CPython 的特性,不是 Python 语言规范(Python 3.13 起有无 GIL 的实验版本)。
第二件事:正解——CPU密集用多进程,IO密集用多线程/异步
搞懂了原理,正解就清晰了:CPU 密集任务用多进程(绕过 GIL 真正并行)、IO 密集任务用多线程或异步、按任务类型选对并发模型。
# ====== 正解一(CPU密集): 用多进程 multiprocessing 绕过 GIL ======
from multiprocessing import Pool
import time
def cpu_heavy(n):
total = 0
for i in range(n):
total += i * i
return total
N = 50_000_000
if __name__ == "__main__": # ★ 多进程必须放在 __main__ 保护下
start = time.time()
with Pool(8) as pool: # 8个进程, 每个进程独立的解释器和GIL
pool.map(cpu_heavy, [N] * 8)
print(f"8进程: {time.time() - start:.2f}s") # ✓ 接近单线程的1/8! 真正并行多核
# → 每个进程有自己的GIL, 8个进程真正在8个核上并行 → CPU密集任务大幅加速!
# 代价: 进程比线程重(创建慢、内存独立、进程间通信有开销)。
# ====== 正解二(IO密集): 用多线程(等IO时释放GIL)======
import threading, requests
def fetch(url):
return requests.get(url).text # 网络IO, 等响应时会释放GIL
urls = [...] * 100
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]
for t in threads: t.start()
for t in threads: t.join()
# → IO密集任务: 一个线程等网络时释放GIL, 其他线程趁机执行 → 多线程有效!
# ====== 正解三(IO密集, 更推荐): 用异步 asyncio ======
import asyncio, aiohttp
async def fetch_async(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_async(session, u) for u in urls]
return await asyncio.gather(*tasks) # 并发发起所有请求
# asyncio.run(main(urls))
# → 单线程内用事件循环并发处理大量IO, 比多线程更轻量、更高效(高并发IO首选)。
# ====== 正解四: 用 concurrent.futures 统一接口(方便切换)======
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# CPU密集 → ProcessPoolExecutor; IO密集 → ThreadPoolExecutor
with ProcessPoolExecutor(max_workers=8) as ex: # CPU密集
results = list(ex.map(cpu_heavy, [N] * 8))
with ThreadPoolExecutor(max_workers=20) as ex: # IO密集
results = list(ex.map(fetch, urls))
# → 两者接口几乎一样, 按任务类型换一个类名即可, 很方便。
# ====== 正解五: CPU密集还可考虑 ======
# - 用 NumPy/Pandas 等底层用C实现的库(它们在C层面计算, 会释放GIL)。
# - 用 Cython / Numba 把热点代码编译成机器码。
# - 把计算下沉到 C 扩展(C扩展可以释放GIL)。
# 核心: 按任务类型选并发模型 —— CPU密集用多进程(每进程独立GIL真正并行多核)、
# IO密集用多线程(等IO释放GIL)或asyncio(更轻量); 用concurrent.futures统一接口方便切换。
修复的核心,是"按任务类型选对并发模型——CPU 密集用多进程,IO 密集用多线程/异步"。正解一(CPU 密集):用多进程 multiprocessing 绕过 GIL——每个进程有自己独立的解释器和 GIL,8 个进程真正在 8 个核上并行,CPU 密集任务大幅加速(接近 1/8 时间);代价是进程比线程重(创建慢、内存独立、进程间通信有开销);注意多进程要放在 if __name__ == "__main__" 保护下。正解二(IO 密集):用多线程——线程等 IO 时释放 GIL、其他线程趁机执行,多线程对 IO 密集有效。正解三(IO 密集,更推荐):用异步 asyncio——单线程内用事件循环并发处理大量 IO,比多线程更轻量高效,高并发 IO 首选。正解四:用 concurrent.futures 统一接口——CPU 密集用 ProcessPoolExecutor、IO 密集用 ThreadPoolExecutor,接口几乎一样,换个类名即可。正解五:CPU 密集还可用 NumPy/Pandas(C 层计算会释放 GIL)、Cython/Numba 编译、C 扩展。归根结底:CPU 密集用多进程(每进程独立 GIL 真正并行)、IO 密集用多线程或 asyncio;用 concurrent.futures 统一接口方便切换。
第三件事:CPU 密集 vs IO 密集,怎么判断
排查后我意识到,选对并发模型的前提是"先判断任务是 CPU 密集还是 IO 密集"。我把判断方法梳理了一遍。
CPU密集 vs IO密集: 怎么判断, 怎么选
# 一、什么是 CPU 密集任务?
# - 大部分时间在"计算"(占用CPU): 数学运算、循环、加解密、压缩、
# 图像/视频处理、机器学习推理(纯计算部分)。
# - 特征: CPU使用率高, 没怎么等外部资源。
# - → 用【多进程】(绕过GIL, 真正并行多核)。
# 二、什么是 IO 密集任务?
# - 大部分时间在"等待"(IO等待, CPU闲着): 网络请求、文件读写、
# 数据库查询、调用外部API、等用户输入。
# - 特征: CPU使用率低, 大量时间在等外部资源响应。
# - → 用【多线程】或【异步asyncio】(等IO时切换去干别的)。
# 三、怎么判断一个任务是哪种?
# - 看它"主要时间花在哪": 花在算(CPU密集) 还是 等(IO密集)。
# - 跑一下看CPU使用率: 高(满核)→CPU密集; 低(核闲着)→IO密集。
# - 看代码: 全是计算/循环 → CPU密集; 有大量 requests/open/db查询 → IO密集。
# 四、选错的后果(本文):
# - CPU密集用多线程: GIL导致无法并行 → 不快反慢(本文的坑!)。
# - IO密集用多进程: 能用但太重(进程开销大), 不如多线程/异步轻量高效。
# - → 选对模型, 性能可能差几倍到几十倍。
# 五、混合任务:
# - 有的任务既有计算又有IO → 按"主要瓶颈"选, 或拆分处理。
# - 如: 用多进程处理CPU部分, 每个进程内用异步处理IO部分。
# 核心: CPU密集(时间花在算/CPU满)用多进程绕GIL并行; IO密集(时间花在等/CPU闲)用多线程或asyncio;
# 先判断任务类型(看时间花在算还是等、看CPU使用率)再选模型, 选错性能差几倍到几十倍。
排查让我明白,选对并发模型的前提是"先判断任务类型"。一、CPU 密集任务:大部分时间在计算(数学运算、循环、加解密、图像处理、ML 推理),CPU 使用率高、没怎么等外部资源 → 用多进程(绕过 GIL 真正并行)。二、IO 密集任务:大部分时间在等待(网络请求、文件读写、数据库查询、调外部 API),CPU 使用率低、大量时间等响应 → 用多线程或异步 asyncio。三、怎么判断?——看主要时间花在哪(算 vs 等)、跑一下看 CPU 使用率(满核=CPU 密集、核闲=IO 密集)、看代码(全是计算=CPU 密集、有大量 requests/open/db=IO 密集)。四、选错的后果(本文):CPU 密集用多线程被 GIL 拖累不快反慢、IO 密集用多进程太重;选对模型性能可能差几倍到几十倍。五、混合任务:按主要瓶颈选或拆分(多进程处理 CPU 部分、进程内异步处理 IO 部分)。下面这张图,是这次多线程不快反慢的成因与解法:
第四件事:并发模型选择速查
这次踩坑后,我把 Python 三种并发方式和任务类型的匹配整理成一张表。
| 并发方式 | 适合任务 | 能并行多核吗 | 开销 |
|---|---|---|---|
| 多线程 threading | IO 密集 | ✗ 不能(GIL) | 较轻 |
| 多进程 multiprocessing | CPU 密集 | ✓ 能(各自GIL) | 重(进程独立) |
| 异步 asyncio | 高并发 IO | ✗ 单线程 | 最轻 |
| 多线程跑CPU密集(本文) | ✗ 错误用法 | ✗ 还更慢 | — |
| 多进程跑IO密集 | △ 能用但浪费 | — | 太重 |
这张表,把"什么任务用什么并发"讲清了:IO 密集用多线程/异步、CPU 密集用多进程。它给我的最大启发是:"并发"不是一个笼统的概念,而是有多种模型,每种模型都有它"擅长的场景"和"致命的盲区";用对了事半功倍,用错了(像本文)反而比不用并发还慢。而选对的关键,在于理解一个核心区分:"并行(parallel)"和"并发(concurrent)"是两回事——"并行"是"真的同时在多个核上执行"(多进程能做到),"并发"是"交替执行、宏观上同时推进"(多线程/异步在等待时切换、宏观上同时处理多个任务)。CPU 密集任务需要的是"并行"(真的同时算),所以要多进程;IO 密集任务需要的是"并发"(等的时候去干别的),所以多线程/异步就够。这让我领悟到:很多概念(并发/并行、同步/异步、进程/线程),只有真正理解了它们"精确的区别和各自解决的问题",才能在面对一个具体任务时,做出正确的技术选型;含混地理解(以为"多线程就是并行加速"),就会像我一样选错、踩坑。精确理解概念,是正确选型的前提。
第五件事:关于 GIL 的常见误解
这次也让我澄清了几个关于 GIL 的常见误解。我把它们梳理了一下。
| 误解 | 真相 |
|---|---|
| Python 多线程没用 | 对IO密集有用(等IO释放GIL),只是对CPU密集没用 |
| 有GIL所以Python不能并发 | 能并发(多线程/异步),只是CPU密集不能并行 |
| GIL是Python语言的缺陷 | 是CPython实现的选择,其他实现可无GIL |
| 多进程总比多线程好 | 看任务,IO密集多线程更轻量 |
| 开越多线程越快 | CPU密集多开线程反而更慢(切换开销) |
| NumPy计算也受GIL限 | NumPy在C层计算会释放GIL,能利用多核 |
这张表,澄清了几个关于 GIL 的常见误解。最关键的两个:"Python 多线程没用"是错的——它对 IO 密集任务很有用(等 IO 时释放 GIL),只是对 CPU 密集没用;"有 GIL 所以不能并发"也是错的——能并发(多线程/异步交替推进),只是 CPU 密集不能并行(同时算)。它给我的最大启发是:对一个复杂的、有适用边界的技术特性(如 GIL),"简单化、一刀切的结论"(如"多线程没用")往往是错的、有害的;真相通常是"它在某些情况下有用、某些情况下没用",需要你理解清楚它的边界,而不是记一个粗暴的结论。我之前对 GIL 的理解,就停留在"听说 Python 有 GIL、多线程不行"这种道听途说的、模糊的结论上,既没搞清它具体限制什么(CPU 密集的并行),也没搞清它不限制什么(IO 密集的并发),结果在该用多线程的地方可能不敢用、在不该用的地方(CPU 密集)用了。这让我领悟到一个学习技术的态度:对任何技术,都要追求"准确而有边界的理解",而非满足于"模糊而绝对的结论";"X 不行/X 很好"这种一刀切的判断,几乎总是不准确的——真相往往是"X 在什么条件下行、在什么条件下不行"。知其然、更知其所以然和其边界——这才是真正掌握一个技术。
第六件事:要做并发提速时,我现在的决策习惯
现在每当我想"用并发提速",我都会按这张图先判断任务类型、再选模型:
这张图的精髓,是"提速前,先判断任务类型,再选对并发模型"。第一步永远是 "判断任务是 CPU 密集还是 IO 密集"(看时间花在算还是等、看 CPU 使用率)。然后:CPU 密集用多进程(ProcessPool)、IO 密集用 asyncio(大量并发)或多线程(中等);CPU 密集还可用 NumPy/Cython 在 C 层释放 GIL 加速。最后一步是我现在的硬习惯:跑一下对比单线程,确认真的快了(这次的坑正是因为想当然以为多线程快、却没实测对比,结果反而更慢)。这套习惯,让我做并发时,从"想加速就上多线程"变成了"先判断任务类型、选对模型、并实测验证"——核心始终是:Python 有 GIL,CPU 密集要用多进程、IO 密集才用多线程/异步,选错不快反慢,要实测验证。
我立下的几条规矩
这场"多线程不快反慢"的事故,换来了我写 Python 并发时,刻进骨子里的几条铁律:
- Python 有 GIL,同时只一个线程执行字节码。多线程无法并行算 CPU。
- CPU 密集任务用多进程。每个进程独立 GIL,真正并行多核。
- IO 密集任务用多线程或异步。等 IO 时释放 GIL,多线程/asyncio 有效。
- 用并发前先判断任务类型。看时间花在"算"还是"等",看 CPU 使用率。
- CPU 密集别用多线程。不仅不并行,GIL 切换开销还让它比单线程慢。
- 用 concurrent.futures 统一接口。ProcessPool/ThreadPool 方便按任务切换。
- 并发提速要实测验证。别想当然,跑一下对比单线程,确认真的快了。
附:一段亲手对比单线程/多线程/多进程的实验
口说无凭。下面这段代码,把同一个 CPU 密集任务用三种方式跑、对比耗时,让 GIL 的影响一目了然:
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def cpu_heavy(n):
total = 0
for i in range(n):
total += i * i
return total
N = 30_000_000
TASKS = 8
def benchmark():
# ====== 1. 单线程: 串行跑8次 ======
start = time.time()
for _ in range(TASKS):
cpu_heavy(N)
t_single = time.time() - start
print(f"单线程串行: {t_single:.2f}s")
# ====== 2. 多线程: 8个线程(受GIL限制)======
start = time.time()
with ThreadPoolExecutor(max_workers=TASKS) as ex:
list(ex.map(cpu_heavy, [N] * TASKS))
t_thread = time.time() - start
print(f"8线程: {t_thread:.2f}s (相对单线程: {t_single/t_thread:.2f}x)")
# ====== 3. 多进程: 8个进程(绕过GIL, 真并行)======
start = time.time()
with ProcessPoolExecutor(max_workers=TASKS) as ex:
list(ex.map(cpu_heavy, [N] * TASKS))
t_process = time.time() - start
print(f"8进程: {t_process:.2f}s (相对单线程: {t_single/t_process:.2f}x)")
if __name__ == "__main__": # ★ 多进程必须在 __main__ 保护下
benchmark()
"""
典型输出(8核机器, CPU密集任务):
单线程串行: 8.20s
8线程: 8.50s (相对单线程: 0.96x) ← 比单线程还慢! GIL限制
8进程: 1.30s (相对单线程: 6.31x) ← 快了6倍多! 真正并行多核
结论一目了然:
- CPU密集任务, 多线程(0.96x)不但没加速反而更慢(GIL+切换开销)。
- 多进程(6.31x)真正利用了多核, 大幅加速。
- (如果把 cpu_heavy 换成 IO 密集任务如 time.sleep/网络请求,
多线程就会有明显加速 —— 因为等待时释放了GIL)
"""
# 核心: 同一CPU密集任务, 单线程8.2s、8线程8.5s(反而更慢)、8进程1.3s(快6倍);
# 跑一遍, GIL让多线程对CPU密集无效、多进程才真正并行 —— 这个对比比任何文字都直观。
这段对比代码,把"GIL 对 CPU 密集任务的影响"变成了三行刺眼的耗时数字。它用同一个 CPU 密集任务,分别跑单线程、多线程、多进程,输出清清楚楚地显示:单线程 8.2s、8 线程 8.5s(相对单线程 0.96x,不但没加速反而更慢!)、8 进程 1.3s(6.31x,快了 6 倍多)。这一组数字,比任何文字解释都更有力地证明了:CPU 密集任务下,多线程被 GIL 拖累(无效甚至更慢),而多进程才能真正利用多核并行加速。而代码注释里那句提醒——如果换成 IO 密集任务,多线程就会有明显加速(因为等待时释放了 GIL)——则点出了"选对模型取决于任务类型"的关键。这,正是我想用这段代码,留给每个 Python 开发者的最后一课:对于"性能"这种"容易凭直觉想当然、但直觉常常错"的东西,最可靠的判断方式永远是"跑个 benchmark、用真实的数字说话"。我这次的坑,根子就在于"我'以为'多线程会快,却从没'测过'";而一个简单的对比 benchmark,就能在几秒钟内,把"以为"和"事实"之间的鸿沟暴露无遗。这也呼应了我在整个系列复盘里反复强调的态度:无论是性能、还是行为、还是各种"我以为是这样"的判断,都别停留在猜测,动手写个最小的实验/benchmark,让数据和现象告诉你真相。尤其是性能优化——它是"反直觉的重灾区",凭感觉优化常常南辕北辙,唯有测量,才能让你优化在真正的瓶颈上、并确认优化真的有效。不测量,不优化;先 benchmark,再下结论。
写在最后
回头看,这场由 GIL 引发的、多线程不快反慢的事故,真正教给我的,远不止"CPU 密集用多进程"这一个知识点。它让我对"想当然地套用经验"的危险,有了又一次深刻的体会。我栽跟头,是因为我把一个在别的语言里(如 Java、C++)成立的"常识"——"多线程能利用多核并行加速 CPU 计算"——想当然地、不加验证地套用到了 Python 上。可 Python 因为 GIL 的存在,这个"常识"偏偏不成立。我用一个"在我熟悉的世界里正确"的认知,去套一个"规则不同的新世界",于是栽了。这让我领悟到一个跨语言、跨技术学习时极其重要的道理:每一门语言、每一个技术,都有它独特的设计、约束和"潜规则";那些在 A 技术里天经地义的"常识",到了 B 技术里,可能因为 B 的某个独特设计(如 Python 的 GIL)而完全失效、甚至相反。所以,当我们带着过往的经验进入一个新领域时,最危险的不是"我不懂的新东西"(那我会去学),而是"我以为我懂、其实在这里不成立的旧经验";这些"过时的常识",会让我们跳过验证、直接套用,从而在新规则下碰壁。这件事给我的最大警示是:对一门新语言/新技术,不要急于套用旧经验,而要先搞清楚"它有哪些独特的设计和约束、哪些我习以为常的常识在这里不成立"(对 Python 而言,GIL 就是这样一个必须知道的"独特约束");并且,对任何"想当然的性能优化",都要动手实测验证,而不是凭旧经验拍脑袋。不让过时的常识误导自己、用实测代替想当然——这,是我用一次"多线程反而更慢"的事故,换来的、关于 Python、也关于"跨技术经验迁移之陷阱"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次想用 Python 多线程加速计算前,先想想 GIL、先判断任务类型,那我对着那个比单线程还慢的 8 线程熬的这大半天,就值了。
—— 别看了 · 2026