☰
Python并发编程:GIL原理与多线程多进程选型指南
2026/10/9 7:29:36 网站建设 项目流程

很多同学第一次接触 Python 并发编程时,大概率都会经历一段困惑期:同样的任务,开 8 个线程跑居然比单线程还慢,换成 8 个进程立刻起飞;可写网络爬虫的时候,多线程又非常好用,甚至比多进程还顺手。网上搜来搜去,到处都在提 GIL、全局解释器锁,但 GIL 到底是什么?它锁住了什么?为什么有的场景要躲着它走,有的场景又完全不受它影响?这篇文章不绕弯子,把 Python 多线程和多进程的选择逻辑、GIL 的工作原理、以及实操中的注意点一次讲透。

无论你是刚入门 Python 的初学者,还是已经写过一段时间脚本、想优化程序性能的开发者,这篇文章都适合你。我会从原理讲到实操,再讲到常见问题和排查心得,尽量做到“看完就能上手”。

1. 这个选择题为什么常年霸榜

1.1 我当年踩的第一个坑

先说个真实经历。早些年我写过一个日志分析工具,要从几十个日志文件里统计关键词出现次数。当时的想法很简单:既然 modern CPU 都是多核,那我把文件分成几份,每个线程处理一份,肯定能快好几倍。于是吭哧吭哧改成了多线程版本,跑起来一看,CPU 占用率只有 20% 左右,耗时反而比单线程还多了 20%。

后来换成了多进程版本,CPU 占用率立刻上去了,处理时间也降到了原来的四分之一。那时候我才意识到:Python 里的线程和进程,不是简单的“都能并行”就能概括的。背地里藏着一个叫 GIL 的东西,它一直决定着你代码的真实执行方式。

这个坑,我相信很多人都踩过。关键是踩完之后,要能搞清楚背后的逻辑,下次才能少走弯路。

1.2 多线程和多进程到底有什么不同

先建立一个基本认知。进程是操作系统分配资源的基本单位,每个进程有自己独立的内存空间、文件描述符等资源;线程是进程内部执行计算的基本单位,同一个进程内的多个线程共享这块内存空间。

所以多进程的优势是:隔离性强,进程之间互相不干扰,一个挂了不影响另一个;缺点是:创建进程开销大、进程间通信复杂、占用内存多。多线程的优势是:创建开销小、线程间共享数据方便;缺点是:共享数据需要加锁保护,而且 Python 里还受制于 GIL。

现实中的电脑硬件早就普及多核了,理论上多个线程可以像多个进程一样,被操作系统调度到不同 CPU 核心上并行执行。但对 CPython(也就是大家日常用的官方 Python 解释器)来说,事情没那么简单——它的内存管理和对象模型不是线程安全的,所以设计了一个全局互斥锁,这就是 GIL。

1.3 搞清楚 GIL,选择就不再是玄学

GIL 的全称是 Global Interpreter Lock,全局解释器锁。它的规则非常粗暴:在同一时刻,CPython 解释器只允许一个线程运行 Python 字节码。也就是说,不管你开多少个线程,在纯 Python 层面,这些线程是“交替执行”的,并不是真正意义上的并行。

很多初学者看到这里就一脸疑惑:那多线程还有啥用?干脆全用多进程得了?

别急。这句话里有个关键限定:运行 Python 字节码。当线程在执行一些不需要持有 GIL 的操作时,比如等待网络响应、读写磁盘、调用某些会主动释放 GIL 的 C 扩展库函数,其他线程是可以真正并行运行的。于是你会发现:IO 密集型的程序用多线程效果很好,CPU 密集型的纯计算程序用多线程就歇菜。明白了这个底层逻辑,选型就不再是玄学,而是有明确依据的工程决策。

2. GIL 的来龙去脉与工作原理

2.1 GIL 锁住的到底是什么

要理解 GIL,就得先理解 CPython 的内存管理。Python 里每个对象,都有一个引用计数(reference count),当引用计数归零时,对象就会被回收。假如没有 GIL,两个线程同时操作同一个对象,就可能出现竞态条件:一个线程正在读取引用计数,另一个线程同时修改了它,最后导致计数错乱,甚至内存泄漏、程序崩溃。

GIL 做的事情,就是保证解释器的核心状态在任何时刻都只有一个线程能修改。具体来说,每个线程在执行 Python 字节码之前,都要先尝试获取 GIL,拿到之后才能执行;执行一小段时间后,释放 GIL,让其他线程有机会运行。这样就从根源上避免了多个线程同时改内存的问题。

代价也随之而来:单线程能充分利用一个核心,但多个线程想要真正跑到不同核心上,却被 GIL 卡在了门口。

注意:GIL 是 CPython 实现层面的特性,不是 Python 语言本身的特性。像 Jython(Java 平台上的 Python 实现)就没有 GIL,IronPython(.NET 平台)也没有。只不过绝大多数人用的都是 CPython,所以讨论 GIL 都默认针对 CPython。

2.2 GIL 获取和释放的时机

GIL 不是一直死死抓住不放的,它有自己的切换机制。官方文档里把这个机制描述得比较晦涩,我用大白话总结一下:

  • 时间片轮转:CPython 里有个默认的“线程切换间隔”,大约 5 毫秒。一个线程运行到时间点后,就会主动释放 GIL,让其他线程去竞争这把锁。
  • IO 操作时释放:当线程执行阻塞型 IO 操作,比如read()、write()、sleep()、网络收发数据,解释器会先释放 GIL,然后进入系统调用。这时候其他线程就能拿到 GIL 继续跑,IO 操作本身也能和其他线程的计算真正的重叠。
  • C 扩展中主动释放:很多常见的计算库,比如 NumPy、Pandas 在底层做大规模数值运算时,会通过 C 扩展接口主动释放 GIL,让纯计算部分在多个核心上并行。这也是为什么你用 NumPy 做矩阵运算时,即便开了多线程,也还是能感受到一定的加速效果。

你可以用sys.getswitchinterval()查看当前解释器的线程切换间隔,用sys.setswitchinterval(seconds)修改它。默认值是 0.005 秒,即 5 毫秒。

import sys print(sys.getswitchinterval()) # 输出 0.005

这个参数一般不建议乱调。调大了,线程的响应变慢;调小了,线程切换开销暴增,得不偿失。默认值在绝大多数场景下已经是一个平衡得很好的值。

2.3 Python 3.2 之后 GIL 有什么变化

很多老文章说 GIL 的时候,还在引用 Python 2.x 时代的信息,讨论的还是“每执行 100 条字节码就切换线程”的老机制。其实 Python 3.2 之后,GIL 的实现已经重写过一次了。

旧版 GIL 是简单的“基于计数的系统”:每执行一定数量的字节码指令就强制切换。这种机制的问题很多:频繁切换导致线程切换开销大,而且一个线程如果执行了很耗时的 C 函数,其他线程会长时间拿不到 GIL,造成明显的停顿。

Python 3.2 重写后的 GIL 引入了一个“请求竞争”机制:主线程每隔 5 毫秒主动释放一次 GIL,如果其他线程之前提出过获取请求,就优先把 GIL 让给它们。这样既保证了 CPU 密集型线程之间相对公平的调度,也减少了不必要的线程间切换开销。到了 Python 3.9 左右,GIL 的实现又做了一些优化,进一步降低了多线程切换的代价,但核心约束依然存在:纯 Python 代码依然不能真正并行。

2.4 GIL 对项目的影响范围有多大

GIL 影响最大的,是那些“纯 Python 实现 + CPU 密集计算 + 多线程并行”这三项叠加的程序。比如:纯 Python 写的图像滤镜算法、大量循环的数值计算、文本处理中的复杂正则匹配,这些都是典型例子。

影响不大的场景有:

  • 网络爬虫、API 请求、文件读写这类 IO 密集型任务,线程在等待期间会释放 GIL。
  • 数据库操作、消息队列消费等场景,同样是阻塞 IO 密集。
  • 调用了 NumPy、OpenCV 等底层会释放 GIL 的库的时候,多线程也能获得一定并行收益。

需要强调一点:即便有 GIL,Python 多线程在 IO 密集场景下的优势依然非常明显。因为多线程的创建和上下文切换成本低,而且线程间共享数据方便,代码写起来也比多进程直观得多。

3. 判定场景,选线程还是选进程

3.1 IO 密集和 CPU 密集,一张表看清

这里给出一个基础但非常有用的分类逻辑:

任务类型特征典型例子首选方案
IO 密集型大部分时间在等待外部资源爬取网页、读写文件、查询数据库、下载图片多线程或协程
CPU 密集型大部分时间在计算音视频编解码、复杂数学运算、大数据量排序、模型训练多进程
混合型既有计算又有等待边下载边解析、边读文件边做统计多进程 + 多线程组合

怎么判断自己是哪种类型?最简单的办法是看程序的 CPU 占用率。跑任务的时候打开任务管理器或者top命令观察:如果 CPU 占用率很低(低于 30%),大概率是 IO 密集;如果某个核心一直是 100%,那就是 CPU 密集。

从工程经验来说,95% 的业务代码是 IO 密集型。因为真正的业务逻辑里,耗时大头往往在数据库查询、第三方 API 调用、文件上传下载这些外部 IO 上。只有数据处理、科学计算、图像处理等领域,才会频繁出现 CPU 密集场景。

3.2 三个问题帮你快速判定选型

我在实际面试和带新人的时候,经常用下面三个问题帮大家做决策:

  1. 任务是真的需要并行,还是只是需要并发?并行是指多个任务在同一时刻一起执行,并发是指多个任务在宏观上看起来同时执行。对 IO 密集来说,我们通常只需要并发——线程在等待时让出 CPU 给别的线程用就行。CPU 密集才需要真正的并行。
  2. 任务之间需要频繁交换数据吗?如果任务之间存在大量中间结果要共享,多线程的共享内存模型会舒服很多。用多进程的话,每传一次数据都要序列化、反序列化,开销很大。
  3. 单任务的计算量有多大?如果单任务本身计算量很小,比如只是从列表里取出一个元素做一下判断,那么多进程的创建和通信开销可能比任务本身还大,这时候老老实实用单线程反而最快。

这些问题想清楚了,方向自然就明确了。

3.3 协程加入之后,怎么选

Python 协程(asyncio)是另一个绕不开的话题。协程比线程更轻量,一个线程里可以跑成千上万个协程,而且协程的切换是用户态控制的,没有操作系统线程切换那么大的开销。

所以实际选型时,我通常的建议顺序是:

  • IO 密集型,且任务数非常多(成百上千):协程优先
  • IO 密集型,任务数适中,或者团队对 asyncio 不熟:多线程
  • CPU 密集型:多进程
  • 复杂系统,IO 密集且内部有大量 CPU 计算:多进程 + 多线程 + 协程的组合

不同语言有个冷知识:Java 里的多线程是真正并行执行的(因为 JVM 没有 GIL 这种全局锁),所以 Java 程序员习惯用线程池搞定大多数并发问题。而 Python 因为 GIL 的存在,经常需要先用协程或进程“绕道”,这其实是 Python 社区的共识,不是你的水平问题。

4. 实操:多线程处理 IO 密集型任务

4.1 用 ThreadPoolExecutor 构建线程池

Python 里最方便的多线程方案,不是手动去threading.Thread管理线程列表,而是用标准库concurrent.futures.ThreadPoolExecutor。它自带线程池、任务队列和结果获取机制,代码非常简洁。

我们以一个真实场景为例:要从 100 个 URL 里下载网页内容并统计大小。先看串行版本:

import time import requests urls = [f"https://httpbin.org/delay/{i % 5}" for i in range(100)] def fetch(url): resp = requests.get(url, timeout=10) return len(resp.content) start = time.time() sizes = [fetch(url) for url in urls] print("串行耗时:", time.time() - start)

再看线程池版本:

from concurrent.futures import ThreadPoolExecutor import time import requests urls = [f"https://httpbin.org/delay/{i % 5}" for i in range(100)] def fetch(url): resp = requests.get(url, timeout=10) return len(resp.content) start = time.time() with ThreadPoolExecutor(max_workers=10) as executor: sizes = list(executor.map(fetch, urls)) print("线程池耗时:", time.time() - start)

我在本地实测过,串行版本大约需要 25 秒,而线程池版本只需要 3 秒左右。原因很简单:每个请求的大部分时间都花在了等待网络响应上,线程在等待时释放了 GIL,其他线程能够并发发起新的请求,整体吞吐量大幅提升。

4.2 线程池大小怎么定

线程池的max_workers参数不是越大越好。对 IO 密集型任务来说,理想的线程数量通常取决于 IO 等待时间和 CPU 处理时间的比例,以及底层资源限制。

一个常用的经验公式是:

最佳线程数 = CPU 核心数 * (1 + 平均等待时间 / 平均计算时间)

但实际项目中没人会精确计算这个比例,大家更多依赖压测。我常用的做法是:

  • 网络请求类任务:从min(32, cpu核心数*5)开始,观察耗时曲线逐步调整
  • 文件读写类任务:线程数不要超过文件句柄上限(一般几百到几千)
  • 数据库连接池 + HTTP 连接池:线程数不要超过连接池上限,不然线程都阻塞在等连接上

这里有个常见的坑:如果你用的是requests库,它底层的 urllib3 连接池默认每个主机最多 10 个连接,线程超过 10 个时多余的线程会排队等连接释放。所以实际优化时,除了调线程数,还要调连接池大小。

import requests from requests.adapters import HTTPAdapter session = requests.Session() adapter = HTTPAdapter(pool_connections=50, pool_maxsize=50) session.mount("https://", adapter)

4.3 多线程的线程安全细节

多线程虽然好用,但共享数据的保护是必修课。list.append()在 CPython 里由于 GIL 的存在,单次操作是原子性的,但多个操作组合起来不是原子性的。

举个例子,一个线程读列表、判断条件、再修改列表,这个过程可能被另一个线程打断,导致条件判断和修改之间的逻辑不一致。解决办法是使用锁(threading.Lock)或者改用线程安全的数据结构(比如queue.Queue)。

import threading counter = 0 lock = threading.Lock() def increment(): global counter for _ in range(100000): with lock: counter += 1 threads = [threading.Thread(target=increment) for _ in range(4)] for t in threads: t.start() for t in threads: t.join() print(counter) # 期望是 400000

注意:如果不加锁,上面代码的结果大概率小于 400000。因为counter += 1在字节码层面上是“读取-加法-写入”三个操作,多线程交替执行时会发生丢失更新。这和 GIL 无关,纯粹是原子性问题。

另一个高频问题:为什么我在子线程里改全局变量,主线程看不到?不是看不到,而是可能读取到旧值。由于 GIL 会在任意字节码指令之间切换,global变量的读写时机完全取决于运气。正确做法还是加锁,或者用queue.Queue、concurrent.futures.Future这类官方推荐的数据传递机制。

5. 实操:多进程处理 CPU 密集型任务

5.1 用 ProcessPoolExecutor 实现真并行

多进程解决的是“CPU 密集型 + 真并行”的需求。每个进程有自己独立的 Python 解释器实例和内存空间,GIL 在进程之间当然也就不起作用。所以多进程可以真正利用多核心 CPU。

看一个 CPU 密集的典型例子:计算从 1 加到 1000 万的平方和。单线程版本:

import time def compute(n): total = 0 for i in range(n): total += i * i return total start = time.time() result = compute(10_000_000) print("单线程耗时:", time.time() - start)

多进程版本:

from concurrent.futures import ProcessPoolExecutor import time def compute(n): total = 0 for i in range(n): total += i * i return total if __name__ == "__main__": n = 10_000_000 tasks = [n // 4] * 4 # 把大任务分成 4 块 start = time.time() with ProcessPoolExecutor(max_workers=4) as executor: results = list(executor.map(compute, tasks)) print("多进程耗时:", time.time() - start) print("结果总和:", sum(results))

在我的 8 核机器上,单线程大约 1 秒,多进程大约 0.3 秒不到。随着任务规模和核数增加,差距会更加明显。

5.2 多进程避不开的序列化开销

多进程虽好,但进程间通信(IPC)成本比线程间共享内存高得多。每次向子进程传递参数、取回结果,都需要经过 pickle 序列化和反序列化。如果传递的数据量大,比如一个几十 MB 的 DataFrame,序列化时间甚至可能超过计算时间。

所以使用多进程时有几个实战原则:

  • 尽量传小的任务描述,而不是大的数据体。比如传一个“文件路径”而不是整个文件内容,让子进程自己读文件。
  • 尽量让每个任务长时间运行,减少进程间通信频率。过程就像快递员送大件包裹——每趟车都装满,比频繁跑空车高效得多。
  • 避免子进程之间需要频繁同步数据。如果任务强依赖彼此的中间结果,多进程的通信开销会让你怀疑人生。

当初我写日志分析程序时,就是把每个文件路径传给子进程,子进程读文件、计算、返回一个字典,最后主进程合并。整个过程进程间传输的只是一堆小字典,速度飞快。如果当时图省事把整个文件内容都塞给子进程,可能就直接退化成单线程性能了。

5.3 子进程的启动方式和平台差异

多进程在不同平台上的启动方式不同,Windows 下默认是spawn,Linux 下默认是fork。区别在于:

  • fork:子进程复制父进程的整个内存映像,启动快,但继承状态可能不一致
  • spawn:子进程从零开始重新导入模块、执行代码,启动慢,但更干净

由于 Windows 的spawn方式会重新执行整个模块,所以多进程代码必须放在if __name__ == "__main__":块里,否则会无限递归创建子进程。这也是很多人跨平台跑多进程代码时最容易踩的坑。

如果你的代码里既有多进程又有全局变量,请注意子进程不会共享父进程的全局变量修改。每个子进程都是父进程的“克隆”(fork 模式)或“新实例”(spawn 模式),改动互不可见。需要跨进程共享数据时,用multiprocessing.Queue、multiprocessing.Pipe或Manager,不要用全局变量。

5.4 混合场景的进程内线程方案

工程上最复杂的一类场景是:主任务需要从多个数据源拉取数据(IO 密集),拉回来之后又要做复杂的数值计算(CPU 密集)。这时候纯多线程或纯多进程都不够用。

我常用的结构是:

  • 主进程里用ThreadPoolExecutor做数据抓取,一个线程负责一个数据源
  • 抓到的数据通过queue.Queue汇总到主进程
  • 主进程把汇总结果拆成几块,交给ProcessPoolExecutor的子进程做计算
  • 子进程返回计算结果后,主进程再统一落库或写文件

这样每一层都使用了最适合的并发模型。注意一点:不要在主进程里既用线程池又用进程池,却互相阻塞等待。更好的做法是让它们通过队列异步衔接,这里可以用multiprocessing.Queue或借助concurrent.futures的 Future 回调函数来做。

6. GIL 的局限与绕过之道

6.1 GIL 对纯计算的影响到底有多大

用一个简单的实验来说话:设计一个纯 CPU 密集的循环任务,分别用单线程、多线程、多进程跑。代码逻辑很简单,就是对一个整数做大量加减乘除运算。

def cpu_bound(n): x = 0 for i in range(n): x += i * 2 - 1 return x # 单线程 start = time.time() for _ in range(4): cpu_bound(10_000_000) print("串行耗时:", time.time() - start) # 多线程 from concurrent.futures import ThreadPoolExecutor start = time.time() with ThreadPoolExecutor(max_workers=4) as ex: list(ex.map(cpu_bound, [10_000_000] * 4)) print("多线程耗时:", time.time() - start) # 多进程 from concurrent.futures import ProcessPoolExecutor start = time.time() with ProcessPoolExecutor(max_workers=4) as ex: list(ex.map(cpu_bound, [10_000_000] * 4)) print("多进程耗时:", time.time() - start)

实测结果非常典型:

方案耗时(约)说明
串行1.0x基线
多线程1.1x ~ 1.3x略慢,因为线程切换有额外开销
多进程0.3x ~ 0.35x接近线性加速

这种场景就是 GIL 影响最大的重灾区。如果项目里大量使用纯 Python 做循环计算,多线程非但没有加速,反而会因为线程调度和竞争拖慢速度。

6.2 有哪些绕过 GIL 的常用方案

既然 GIL 带来了麻烦,社区自然想了很多办法绕开它:

  • 换解释器:用 PyPy(Python 的 JIT 实现)跑纯计算,PyPy 由于实现方式不同,也在做 STM(软件事务内存)方向,不过目前生产环境还是 CPython 为主。
  • 用 C 扩展释放 GIL:在 C 扩展函数内部通过Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS显式释放 GIL。很多成熟库(NumPy、Pandas 部分操作)就是这么做的。
  • 把计算任务交给第三方库:能用 NumPy 向量化操作就不要用 Python 循环,能用 Cython、Numba 预编译就不要直接写纯 Python。Numba 的@njit装饰器可以把 Python 函数翻译为机器码,并在运行时使用多个线程而受 GIL 影响小。
  • 多进程兜底:这是最简单、最通用、最容易理解的方案,用进程数代替线程数,用内存换并行。

正确思路不是所有情况都去“绕过 GIL”,而是:先分析任务类型,再用合适的工具。如果纯 Python 的 for 循环占了主要计算,先想想能不能改写为 NumPy 或者用多进程拆任务,而不是一上来就试图“黑”进解释器。

重要提示:任何领域如果需要使用多线程并追求真正的 CPU 并行,聪明的方式是选择那些在 C 语言层面已经做过 GIL 释放优化的库。如果你的任务本身是等待 IO,那么多线程依然是最佳选择之一。

6.3 面向未来的 no-GIL 版本

Python 社区其实一直在推进“去掉 GIL”的进程。PEP 703 提出的“free-threaded”模式,让 Python 从 3.13 开始可以构建为不带 GIL 的版本。在这个模式下,多线程的 CPU 并行成为可能,但这还属于比较新的特性,很多 C 扩展库需要专门适配才能保证线程安全。

我给团队做技术选型时,一般不会因为 no-GIL 的进展而立刻改架构。原因很简单:当前生产环境的生态、第三方库兼容性、运维标准还是以 CPython 默认版本为主。未来 3、5 年后如果默认版本直接支持 no-GIL,那是顺理成章的事情,但现阶段我们的项目还在跑 CPython 3.10-3.12。技术人要拥抱变化,但不能让未来的可能性绑架今天的工程决策。

7. 常见问题与排查技巧实录

7.1 高频问题速查表

我把自己带项目和带新人时遇到的高频问题整理在这里:

现象可能原因解决思路
多线程跑纯计算反而变慢GIL 切换开销换成多进程或使用 Numba/NumPy 加速
多线程请求接口时速度上不去HTTP 连接池太小 / DNS 解析阻塞调大连接池、使用 Session、开启持久连接
多进程启动特别慢Windows spawn 模式导致重复导入模块代码放入if __name__ == "__main__",减少顶层重逻辑
线程里改全局变量,结果不对原子性问题加threading.Lock或使用queue.Queue
多进程传大 DataFrame 很慢pickle 序列化开销过大改为传文件路径或使用内存映射文件
asyncio 和 ThreadPoolExecutor 一起用时卡死线程中运行了同步阻塞代码将同步调用丢到 executor,避免阻塞事件循环
Python 多进程反复报 RuntimeError跨平台启动方式差异检查是否漏写了if __name__ == "__main__"

7.2 我建议的选型路线图

综合所有经验,我给不同水平的开发者一个直接可用的选型路线图:

  1. 先确认任务的瓶颈。用cProfile或简单的时间统计,找出耗时最长的部分。
  2. 判断是 IO 密集还是 CPU 密集。CPU 占用率低且任务在等网络/磁盘,是 IO 密集;CPU 占用率持续高,是 CPU 密集。
  3. IO 密集的:任务量大用协程,任务量适中或团队不熟悉 asyncio 用线程池。
  4. CPU 密集的:能向量化则用 NumPy/Numba,不能向量化则用多进程。
  5. 混合型:多进程负责 CPU 部分,线程/协程负责 IO 部分,用队列衔接。
  6. 怀疑性能问题时,优先做压测和 Profiling,不要靠猜。

另外一个心得:不要过度优化。如果你的程序单次运行只要 1 秒,完全没必要为了并发而并发。并发引入的代码复杂度、调试难度、资源占用,都可能是负收益。我见过不少项目,为了“展示技术”强行上多线程,结果 bug 比收益还多。

7.3 几个值得分享的实操小技巧

最后分享几个常规文档里不太写的东西:

第一,写多线程或异步代码时,优先用concurrent.futures和asyncio这类高层 API,不要直接操作threading.Thread和multiprocessing.Process。高层 API 帮你处理了任务队列、异常传递和结果收集,代码的可读性和稳定性都更好。

第二,在 Python 3.8+ 里,ThreadPoolExecutor和ProcessPoolExecutor都支持传入initializer参数,可以在工作线程/进程启动时做一些初始化操作,比如创建数据库连接。这个设计比每次任务里重连数据库高效很多。

第三,排查并发 bug 时,最有效的工具不一定是加日志,而是记录并发执行顺序。我习惯在线程池的每个任务开头打一行start日志,结尾打一行end日志,中间就不打了。任务一多,日志文件的顺序自然会告诉你问题出在哪个阶段。数据竞争类 bug 极难从单次运行里定位,需要多次复现 + 日志对比。

第四,压测务必在外网环境下做。本地 localhost 的请求响应太快,线程并发优势体现不出来,容易误导你做出错误选型。

我自己在实际操作中的体会是:选多线程还是多进程,本质上不是“哪个更牛”的问题,而是“你的任务到底卡在哪里”的问题。GIL 的存在确实给 Python 并发带来了一层额外的理解门槛,但一旦你掌握了它,Python 的并发编程从一门玄学就变成了一套可以计算、可以预测、可以测试的工程知识。这也是为什么这篇内容值得多读几遍,并且配合自己的小项目亲自动手跑一遍。纸上得来终觉浅,把上面的代码示例在自己的机器上完完整整跑一遍,你体会到的东西会远超你读十遍文章的收获。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询