Python的GIL把我坑惨了,原来多线程和多进程的区别这么大

简介: 本文以一次图像处理性能优化失败为引,深入浅出解析Python的GIL(全局解释器锁)机制:为何多线程对CPU密集型任务无效,却适合I/O密集型场景;对比多进程如何真正实现并行,并详解选型策略、实战坑点及Python 3.13自由线程新进展。(239字)

一个让我怀疑人生的性能优化

前年接了一个图像批处理项目。每天有上万张商品图片需要做缩放、水印和格式转换。我心想:“这还不简单?多线程并行处理,分分钟搞定。”

代码写得行云流水:

import threading
from PIL import Image

def process_image(image_path):
   img = Image.open(image_path)
   img = img.resize((800, 800))
   # 加水印、转格式...
   img.save(f"processed_{image_path}")

images = get_all_images()  # 一万张图
threads = []
for path in images:
   t = threading.Thread(target=process_image, args=(path,))
   t.start()
   threads.append(t)
for t in threads:
   t.join()

开了20个线程,信心满满地跑起来。结果一看CPU监控——8核的服务器,CPU使用率只有25%左右,跟单线程跑没啥区别。处理速度也没快多少,一万张图还是跑了好几个小时。

同事路过看了一眼:“你用多线程跑CPU密集型任务?不知道GIL吗?”

我:“GIL是什么?”

那天下午,我把GIL、多线程和多进程的区别彻底研究了一遍。今天把这些东西讲清楚,希望你下次遇到类似问题别像我一样白忙活。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (47).png

第一步:GIL到底是什么?

GIL的全称是Global Interpreter Lock,全局解释器锁。它是CPython解释器(就是咱们平时用的那个Python)里的一个互斥锁机制。

它的作用简单粗暴:同一时刻,只有一个线程能执行Python字节码

也就是说,即使你的电脑有8核16核,跑Python多线程程序的时候,同一时间只有一个核心在工作,其他核心都在旁边看着。

听到这儿你可能想骂人了:“那Python的多线程不就是个摆设吗?”

别急,事情没那么简单。

第二步:GIL为什么存在?

GIL不是Python设计者脑子进水搞出来的东西。恰恰相反,它是一个有意为之的设计决策

Python的内存管理用的是引用计数——每个Python对象都有一个计数器,记录有多少个地方引用了它。当计数器归零时,对象就被回收了。

问题来了:在多线程环境下,如果两个线程同时修改同一个对象的引用计数,就会出现竞争条件——计数可能只增加了一次而不是两次,导致对象永远无法被回收,造成内存泄漏

解决这个问题有两种方案:

  1. 给每个对象加锁——细粒度锁,性能好但实现极其复杂,容易死锁
  2. 给整个解释器加一把大锁——简单粗暴,但限制了并行

Python选择了方案二。GIL就像是一个门卫,牢牢掌控着Python字节码的执行权,确保同一时刻只有一个线程能进门。

在单核CPU时代,这个设计完全没问题——反正同一时间本来就只有一个线程能用CPU。但多核普及之后,GIL就成了Python多线程的“紧箍咒”。

第三步:多线程到底有没有用?

有用,但要看场景。

GIL只限制Python字节码的执行。当线程执行I/O操作(比如网络请求、文件读写、数据库查询)时,它会主动释放GIL。其他等待的线程就可以趁机获取GIL并执行。

这就好比一个食堂只有一个打饭窗口(GIL)。如果你要做的只是“站在窗口前等饭”(I/O等待),那多几个人排队也没问题——反正大家都在等,谁先谁后差别不大。但如果你要做的是一人霸占窗口做复杂操作(CPU计算),那其他人就只能干等着。

I/O密集型任务(网络爬虫、文件读写、数据库操作):多线程很有效

  • 线程大部分时间在等待外部响应,不占用CPU
  • 等待时释放GIL,其他线程可以运行
  • 实测:20个网络请求,4线程比单线程快约4倍

CPU密集型任务(图像处理、数值计算、加密解密):多线程基本没用

  • 线程一直在执行计算,不释放GIL
  • 多个线程轮流抢一把锁,加上切换开销,可能比单线程还慢
  • 实测:计算质数,4线程比单线程还慢0.5秒

有测试数据显示:在4核CPU上跑CPU密集型任务,单线程耗时10.2秒,4线程耗时10.0秒——几乎没有提升。多出来的线程除了抢锁和切换上下文,什么都没干。

第四步:多进程——绕过GIL的真正并行

既然多线程被GIL卡死了,那怎么办?

答案是多进程

每个Python进程都有自己独立的解释器和独立的内存空间。每个进程都有自己的GIL,互不干扰。所以在多核CPU上,多个进程可以真正做到同时运行——一个进程跑在一个核心上。

用多进程改造一下我的图像处理代码:

from multiprocessing import Pool

def process_image(image_path):
   # 同样的处理逻辑
   pass

if __name__ == "__main__":
   images = get_all_images()
   with Pool(8) as p:  # 8核CPU开8个进程
       p.map(process_image, images)

改完之后,CPU使用率直接飙到95%以上,处理时间从几个小时缩短到了几十分钟。

实测数据更能说明问题:在4核CPU上计算100万以内质数,单线程3.2秒,4线程反而要6.1秒(线程切换开销+GIL竞争),但4进程只用了1.8秒,接近4倍加速。另一个测试显示,4进程方案在4核CPU上能达到3.66倍的加速比。

多进程的好处:

  • 真正利用多核CPU
  • 没有GIL干扰
  • 每个进程内存隔离,不会相互影响

多进程的代价:

  • 进程创建开销大(约100微秒到1毫秒,线程只需1-5微秒)
  • 进程间通信需要序列化(pickle),比较麻烦
  • 内存占用大——100个线程增加约15MB内存,100个进程增加约800MB
  • 数据共享不如线程方便

第五步:到底该选哪个?

一张图说清楚:

场景 推荐方案 原因
网络爬虫、API调用 多线程 I/O等待时释放GIL,轻量高效
文件读写、数据库操作 多线程 同上
图像/视频处理 多进程 CPU密集型,需要真并行
数值计算、机器学习 多进程 充分利用多核
加密解密、压缩解压 多进程 CPU密集型

有个真实的案例:某图像处理项目,用多进程处理1080P视频转码,速度从单线程的45分钟缩短到了12分钟。还有个爬虫项目,用多线程后日抓取量从10万页提升到了80万页。

简单粗暴的判断标准:如果代码大部分时间在等(等网络、等磁盘、等数据库),用多线程;如果代码大部分时间在算(循环、计算、处理),用多进程

第六步:还有几个坑

坑一:别在Windows上瞎用多进程

Windows创建进程的开销比Linux大得多,而且multiprocessing在Windows上需要用if __name__ == "__main__"保护,不然会无限递归创建进程。

坑二:进程间传数据要序列化

多线程共享内存,传递数据直接传引用就行。多进程不行——每个进程内存独立,传递数据需要pickle序列化。如果数据太大,序列化本身就很耗时。

坑三:不是所有第三方库都释放GIL

有些C扩展库(比如某些旧版本的加密库)在执行时不释放GIL,即使在做I/O操作也不放。这就导致多线程在这些库上完全没用。用之前最好确认一下。

坑四:进程数不是越多越好

进程数一般等于CPU核心数就够了。开太多进程,上下文切换的开销反而会拖慢速度。

顺便说一句:GIL快没了

Python 3.13已经推出了实验性的自由线程(Free-Threading)模式,可以禁用GIL。PEP 703正式提出了让GIL变成可选项的方案。

不过目前还是实验阶段,性能差距还有待优化(早期版本自由线程比非自由线程慢约40%,现在已经降到10%以内了)。而且就算GIL最终被移除,多线程和多进程的适用场景也不会完全重叠——内存模型、通信方式这些本质区别依然存在。

所以,理解GIL、多线程和多进程的区别,在今天依然很有必要。

总结

一句话记住区别:多线程是“一个人干多个活,但一次只能干一个”;多进程是“多个人各干各的,互不干扰”

用我那个图像处理的例子来总结:

  • 多线程:一个人同时操作8台机器,但每次只能操作一台——看起来忙,实际上效率没提高
  • 多进程:8个人各操作一台机器——真正的同时干活,效率翻倍

现在我写代码但凡涉及到并发,第一反应就是先判断:“这任务是等还是算?”等就用线程,算就用进程。想清楚再动手,少让CPU闲着。

目录
相关文章
|
25天前
|
IDE 数据可视化 API
【2026最新】OpenGL下载、安装配置、使用一篇搞定(附安装包+实例)
OpenGL是跨平台图形API,广泛用于游戏、CAD与科学可视化。本文详解在VS2026中配置GLFW环境:下载组件、设置包含目录与库路径、链接glfw3.lib,最后运行彩色三角形实例,助初学者快速入门图形编程。(239字)
|
24天前
|
人工智能 IDE 开发工具
最新版 OpenCode(AI 编程工具) 功能介绍
在现代化软件开发流程中,传统IDE插件、网页端AI编程工具普遍存在三大痛点:平台绑定严重、模型兼容性差、无法深度融入终端工程化流程。多数AI编码工具仅支持固定模型厂商,难以适配本地私有模型、第三方开源模型混用场景;网页端工具依赖浏览器环境,无法直接操作本地项目文件、执行系统命令、联动工程脚本;常规IDE插件功能单一,仅能实现基础补全,缺失项目规划、多文件批量重构、代码诊断、工程文档生成等高阶能力。
288 0
|
24天前
|
人工智能 自然语言处理 语音技术
阿里云Token Plan支持哪些AI模型?个人版和团队版有区别吗?
阿里云百炼TokenPlan分个人版与团队版:个人版(39–499元/月)支持Qwen3.8-preview、GLM-5.2、万相图像及HappyHorse视频等主流模型;团队版(198元起/坐席/月)额外支持qwen-image-2.0、Kimi-K2.7、GLM-5、MiniMax-M2.5等20+款多模态模型,满足企业级高阶需求。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
202 0
成功解决ForkingPickler(file, protocol).dump(obj) TypeError: can't pickle Environment objects
成功解决ForkingPickler(file, protocol).dump(obj) TypeError: can't pickle Environment objects
成功解决ForkingPickler(file, protocol).dump(obj) TypeError: can't pickle Environment objects
|
9月前
|
运维 监控 Linux
守护你的服务器(Linux进程监控与实时告警入门指南)
本文介绍Linux进程监控的重要性及基础实现方法,通过Shell脚本检测进程状态并记录告警日志,结合Cron定时任务实现自动化监控,适合运维新手入门。
|
安全 数据库连接 开发者
深入理解Python中的上下文管理器和with语句
本文深入讲解了Python中的上下文管理器与`with`语句。上下文管理器是一种用于封装代码块进入和退出逻辑的工具,通过定义`__enter__`和`__exit__`方法实现资源的安全管理和异常处理。文章还介绍了如何自定义上下文管理器、使用`contextlib`模块简化创建过程,以及从Python 3.7起支持的异步上下文管理器。这些工具能帮助开发者编写更简洁、安全的代码,有效管理资源和异常。
428 0
|
UED Python
Python requests库下载文件时展示进度条的实现方法
以上就是使用Python `requests`库下载文件时展示进度条的一种实现方法,它不仅简洁易懂,而且在实际应用中非常实用。
815 1
|
存储 缓存 算法
RAID 的镜像是一种冗余技术
镜像是冗余技术的一种,通过在不同磁盘上创建数据的完整副本,提供数据保护。这种方法无需额外计算和校验,故障恢复迅速,支持并发读取,提高读I/O性能,但写入性能受影响。镜像技术虽提供高数据安全性,却需双倍存储空间,成本较高,适用于关键数据保护。此外,镜像可通过“拆分”实现几乎零备份窗口的数据备份。
673 4
|
消息中间件 数据采集 运维
一份运维监控的终极秘籍!监控不到位,宕机两行泪
【10月更文挑战第25天】监控指标的采集分为基础监控和业务监控。基础监控涉及CPU、内存、磁盘等硬件和网络信息,而业务监控则关注服务运行状态。常见的监控数据采集方法包括日志、JMX、REST、OpenMetrics等。Google SRE提出的四个黄金指标——错误、延迟、流量和饱和度,为监控提供了重要指导。错误监控关注系统和业务错误;延迟监控关注服务响应时间;流量监控关注系统和服务的访问量;饱和度监控关注服务利用率。这些指标有助于及时发现和定位故障。
1280 2