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

发布时间:2026/7/22 15:42:37
Python的GIL把我坑惨了,原来多线程和多进程的区别这么大 一个让我怀疑人生的性能优化前年接了一个图像批处理项目。每天有上万张商品图片需要做缩放、水印和格式转换。我心想“这还不简单多线程并行处理分分钟搞定。”代码写得行云流水import threading from PIL import Image def process_image(image_path): img Image.open(image_path) img img.resize((800, 800)) # 加水印、转格式... img.save(fprocessed_{image_path}) images get_all_images() # 一万张图 threads [] for path in images: t threading.Thread(targetprocess_image, args(path,)) t.start() threads.append(t) for t in threads: t.join()开了20个线程信心满满地跑起来。结果一看CPU监控——8核的服务器CPU使用率只有25%左右跟单线程跑没啥区别。处理速度也没快多少一万张图还是跑了好几个小时。同事路过看了一眼“你用多线程跑CPU密集型任务不知道GIL吗”我“GIL是什么”那天下午我把GIL、多线程和多进程的区别彻底研究了一遍。今天把这些东西讲清楚希望你下次遇到类似问题别像我一样白忙活。第一步GIL到底是什么GIL的全称是Global Interpreter Lock全局解释器锁。它是CPython解释器就是咱们平时用的那个Python里的一个互斥锁机制。它的作用简单粗暴同一时刻只有一个线程能执行Python字节码。也就是说即使你的电脑有8核16核跑Python多线程程序的时候同一时间只有一个核心在工作其他核心都在旁边看着。听到这儿你可能想骂人了“那Python的多线程不就是个摆设吗”别急事情没那么简单。第二步GIL为什么存在GIL不是Python设计者脑子进水搞出来的东西。恰恰相反它是一个有意为之的设计决策。Python的内存管理用的是引用计数——每个Python对象都有一个计数器记录有多少个地方引用了它。当计数器归零时对象就被回收了。问题来了在多线程环境下如果两个线程同时修改同一个对象的引用计数就会出现竞争条件——计数可能只增加了一次而不是两次导致对象永远无法被回收造成内存泄漏。解决这个问题有两种方案给每个对象加锁——细粒度锁性能好但实现极其复杂容易死锁给整个解释器加一把大锁——简单粗暴但限制了并行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闲着。