我用多线程跑 CPU 密集的计算想给程序加速,结果开了好几个线程不但没快、反而比单线程还慢,我盯着这个反常的结果查了大半天才搞懂 GIL 的深度复盘

我有段 CPU 密集的计算,想用多线程加速,机器有好几个核,开 4 个线程本以为快 4 倍。结果不但没快、反而比单线程还慢!深究才懂 CPython 有 GIL(全局解释器锁):任何时刻只允许一个线程执行 Python 字节码,4 个线程没法真并行,只能轮流抢 GIL、交替执行,同一刻只有一个在干活,还白搭了线程切换开销。多线程对 CPU 密集起不到并行加速(被 GIL 卡成串行),它真正擅长的是 IO 密集(等 IO 时释放 GIL)。这篇从 GIL 让多线程不能并行执行字节码讲起,到 CPU 密集用多进程/IO 密集用多线程异步的正解、CPU 密集 vs IO 密集怎么分、并发选型全景,以及那句最戳心的——每个工具有它的适用场景,为问题选对工具,别拿锤子拧螺丝。

我用多线程跑 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 并发的注意事项,认真地立成了几条规矩:

  1. 记牢 GIL:多线程不能并行执行 Python 字节码。同一时刻只有一个线程跑 Python 代码。
  2. CPU 密集用多进程。multiprocessing/ProcessPoolExecutor,每进程独立 GIL,真并行多核。
  3. IO 密集才用多线程/异步。等待时让出 GIL,能并发;高并发用 asyncio 更省资源。
  4. 先分清 CPU 密集还是 IO 密集。看瓶颈在"算"还是"等",这是选并发方式的第一步。
  5. CPU 密集也可用 numpy 等。底层 C 实现、计算时释放 GIL,向量化往往又快又能并行。
  6. 控制并发度、注意多进程代价。进程数约等于核数;进程重、IPC/序列化有成本,任务够大才划算。
  7. 别把工具当万能。为问题选对工具,理解它擅长/不擅长什么,别拿锤子拧螺丝。

写在最后

这次"我用多线程跑 CPU 密集、结果不快反慢"的经历,是我在 Python 路上,一次很经典、也很受用的成长。它教给我的,远不止"CPU 密集用多进程"这一条具体的技术经验,更是一个普适的工程智慧——每个工具,都有它的适用场景;为问题,选对工具,别拿锤子拧螺丝。我那个不快反慢的多线程,根源就在于,我把"多线程"当成了"并行加速"的万能工具,却不知道,在 Python 里,因为 GIL,它真正擅长的,是 IO 密集的并发等待,而对 CPU 密集的并行计算,恰恰最无能为力——我把它,用在了它最不擅长的地方。

所以,当你要用某个工具、某项技术去解决问题时,请别想当然地以为它"万能",而要先搞清楚两件事:一是,这个工具,到底擅长解决什么问题、有什么前提和局限;二是,我手上这个问题,它的特征是什么(比如,是 CPU 密集还是 IO 密集);然后,为这个问题,选一个真正匹配的工具就像 Python 的并发,你只要先分清任务是"瓶颈在算"还是"瓶颈在等",就知道该用多进程、还是多线程/异步,绝不会再经历那种"开了多线程、却不快反慢"的反常。从"把工具当万能"到"为问题选对工具",从"并发就是多线程"到"按 CPU/IO 密集选并发模型",是从一个"会用并发 API"的开发,走向一个"懂原理、能选对工具"的工程师,必经的修炼。愿你解决的每一个问题,手里握着的,都是那把最趁手的工具;也愿你我,永远先看清问题的样子,再去挑工具——而不是拿着一把锤子,把所有问题,都看成钉子。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的 Agent 要调十几个工具才能完成一个任务,它老老实实一个接一个地串行调,结果慢得用户都快等睡着了、最后发现那些工具大多本可并行的深度复盘

2026-6-2 1:07:00

技术教程

我用 JavaScript 算钱,0.1 加 0.2 居然不等于 0.3,算出来的金额总是差那么一点点,我对着这串诡异的小数尾巴排查了大半天的深度复盘

2026-6-2 1:19:55

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索