news 2026/8/30 5:08:07

多线程绕不开 GIL,多进程凭什么能绕开?一文讲透 multiprocessing

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多线程绕不开 GIL,多进程凭什么能绕开?一文讲透 multiprocessing

「Python 进阶之路」系列 Day18

写在前面

Day16、Day17 反复验证了一个结论:CPU 密集型任务用多线程没有加速效果,因为 GIL 让同一时刻只有一个线程能执行 Python 字节码。那如果真的需要用 Python 做并行计算,出路在哪?答案是multiprocessing——今天这篇讲清楚它凭什么能绕开 GIL,以及绕开之后要付出什么新的代价。


一、是什么:进程与线程的核心区别

线程模型

单个进程共享的内存空间

线程1

线程2

进程模型

进程1
独立内存空间

进程2
独立内存空间

线程共享同一个进程的内存空间(Day17 讲的锁、竞态条件都是因为这个共享),而多个进程各自拥有独立的内存空间,互不干扰multiprocessing模块提供了和threading接口非常相似的Process类,但创建出来的是真正独立的操作系统进程,不是线程。


二、为什么能绕开 GIL:每个进程有自己独立的解释器

GIL 是单个 CPython 解释器实例内部的限制——每个进程都会启动一份独立的解释器实例,也就有一份自己独立的 GIL,进程之间的 GIL 互不影响。这就是多进程能绕开 GIL 限制、真正利用多核 CPU 并行计算的根本原因。

代价也很明确:进程间不共享内存,数据没法像多线程那样直接读写同一个变量,需要通过专门的进程间通信(IPC)机制传递;而且创建/切换进程的开销比线程大得多,因为要复制一份独立的运行环境。


三、怎么用

1. 实测:多进程真的能获得并行加速

importtimeimportmultiprocessingasmpdefcpu_task(n):count=0for_inrange(n):count+=1returncountif__name__=="__main__":N=15_000_000start=time.perf_counter()cpu_task(N);cpu_task(N)t_single=time.perf_counter()-start start=time.perf_counter()p1=mp.Process(target=cpu_task,args=(N,))p2=mp.Process(target=cpu_task,args=(N,))p1.start();p2.start()p1.join();p2.join()t_multi=time.perf_counter()-startprint(t_single,t_multi,t_multi/t_single)

实测结果:单进程顺序执行两个任务耗时0.538s,两个进程并发执行耗时0.349s,比值0.65。对比 Day16 里同样的 CPU 密集型任务用多线程测出的比值(0.87~0.98,几乎没有加速),多进程的 0.65 明显好得多——虽然没有达到理论上限(两个核心完全并行应该接近 0.5),因为进程创建本身有一定开销,但确实拿到了真实的并行计算收益。

2. 进程间不共享内存

x=1defmodify_var(_):globalx x=999print(f"子进程里 x 被改成了:{x}")if__name__=="__main__":print("主进程修改前 x =",x)p=mp.Process(target=modify_var,args=(None,))p.start()p.join()print("主进程里 x 还是:",x)

实测输出:子进程里确实把x改成了999,但主进程里的x完全没有受到影响,还是原来的1。这是因为子进程拿到的是主进程内存的一份独立拷贝,子进程对这份拷贝的任何修改都不会反映回主进程——这一点和多线程完全不同,多线程共享同一份内存,一个线程改了变量,其他线程立刻能看到。

3. 进程间通信:Queue

既然不能直接共享变量,进程之间要传递数据需要用专门的 IPC 机制,multiprocessing.Queue是最常用的一种:

defproducer(q):foriinrange(5):q.put(i*i)if__name__=="__main__":q=mp.Queue()p=mp.Process(target=producer,args=(q,))p.start()p.join()results=[]whilenotq.empty():results.append(q.get())print(results)# [0, 1, 4, 9, 16]

multiprocessing.Queuequeue.Queue(Day17 提到的线程安全队列)接口相似,但底层实现完全不同——它能跨进程边界传递数据,本质是通过操作系统提供的管道/共享内存机制配合序列化(pickle)实现的。

4. Pool 进程池与共享数据 Value

批量处理任务时,用进程池比手动管理一堆Process对象更方便:

defcpu_square(n):returnn*nif__name__=="__main__":withmp.Pool(processes=4)aspool:results=pool.map(cpu_square,range(10))print(results)# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

如果确实需要在进程间共享一份简单的数据(不是完整的对象),可以用multiprocessing.Value(或Array),它底层用共享内存实现,同样需要配合锁保证操作安全:

defincrement(val,times):for_inrange(times):withval.get_lock():# 依然需要加锁,多进程访问共享数据同样有竞态问题val.value+=1if__name__=="__main__":shared_val=mp.Value('i',0)procs=[mp.Process(target=increment,args=(shared_val,10000))for_inrange(4)]forpinprocs:p.start()forpinprocs:p.join()print(shared_val.value)# 40000,和期望值一致(因为加了锁)

5. 容易踩的坑:ifname== “main” 保护

multiprocessing代码里几乎总要写if __name__ == "__main__":把创建进程的逻辑包起来,不写会直接报错:

# 没有保护的脚本importmultiprocessingasmpdefworker():print("子进程在运行")p=mp.Process(target=worker)p.start()p.join()

实际运行会直接报错:

RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase. ... if __name__ == '__main__': freeze_support() ...

原因:macOS/Windows 默认用spawn方式创建子进程——子进程会重新启动一个全新的 Python 解释器,并重新import一遍主模块的代码来"恢复"运行环境。如果创建进程的代码没有被if __name__ == "__main__":保护起来,子进程重新执行这个模块时会再次跑到p.start()这一行,尝试再创建一个孙子进程——理论上会无限递归下去,Python 检测到这种"还没完成初始化就试图开子进程"的情况,直接主动抛出RuntimeError来阻止这个问题,而不是真的死循环创建进程。


四、面试追问

Q1:multiprocessing怎么绕开 GIL?

GIL 是单个 CPython 解释器实例内部的限制,每个进程都会启动一份独立的解释器实例,也就有一份自己独立的 GIL,进程之间的 GIL 互不影响。因此多个进程可以真正同时执行 Python 字节码,利用多核 CPU 做并行计算,而不受单个 GIL 的串行化限制。

Q2:进程和线程的核心区别是什么?

线程共享同一个进程的内存空间,Day17 讲的锁、竞态条件都源于这种共享;多个进程各自拥有独立的内存空间,互不干扰,一个进程修改的数据不会影响到另一个进程,数据需要通过专门的进程间通信机制传递。

Q3:多进程之间怎么共享数据?

常见方式有:multiprocessing.Queue,一个跨进程的队列,适合传递一系列数据(生产者-消费者模式);multiprocessing.Value/Array,基于共享内存实现的简单数据共享,操作时同样需要配合锁来保证多进程访问时不出现竞态问题。

Q4:为什么multiprocessing代码里要用if __name__ == "__main__":保护?

macOS/Windows 默认用spawn方式创建子进程,子进程会重新启动一个全新的解释器并重新import主模块来恢复运行环境。如果创建进程的代码没有这层保护,子进程重新执行模块时会再次触发创建进程的逻辑,导致递归创建进程,Python 会检测到这种情况并直接抛出RuntimeError来阻止它,而不是真的无限递归下去。

Q5:什么场景选多进程,什么场景选多线程?

CPU 密集型的纯 Python 计算任务应该选多进程,能绕开 GIL、真正利用多核 CPU;I/O 密集型任务(网络请求、文件读写、数据库查询)更适合选多线程,因为创建/切换线程的开销比进程小得多,而且 I/O 等待期间会释放 GIL(Day16 讲过),没必要为 I/O 密集型任务承担多进程带来的额外开销和进程间通信的复杂度。


下一篇预告

Day19 讲 asyncio 协程入门——event loop、async/await到底是怎么运作的,这是应对大量 I/O 密集型任务时,比多线程更轻量的另一种并发方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 5:05:27

Anthropic与OpenAI同台Disrupt 2026:开发者如何备战API评测与模型选型

Anthropic 和 OpenAI 的高管将同时出现在 TechCrunch Disrupt 2026 的 AI 舞台上,这不只是两个头部 AI 实验室的又一次同框,更像是 2026 年模型能力、API 生态、Agent 化服务和合规策略的一次正面对话。如果你平时就在调 Claude 或 GPT 的接口、做 Agent…

作者头像 李华
网站建设 2026/8/30 5:04:40

AI推荐为何更可信却下单更慢?用户信任与决策时间的数据分析实战

最近在分析推荐系统转化数据时,发现了一个很有意思的现象:用户对 AI 推荐给出的信任评分,明显高于对网红和短视频带货的信任评分;可点击率、加购率都挺正常,唯独从“看到推荐”到“真正下单”的决策时间,AI…

作者头像 李华
网站建设 2026/8/30 5:04:25

Skill.md与Llms.txt:LLM应用配置文件的定位、格式与部署实践

这几年 LLM 应用里的文本配置文件越来越多,最容易让人混淆的两份就是 Skill.md 和 Llms.txt。名字都很短,都跟 AI 有关系,但定位完全不同:Skill.md 是给 Agent 看的“操作手册”。它告诉 AI 某个技能该怎么触发、分几步执行、输出…

作者头像 李华
网站建设 2026/8/30 5:02:42

Cloudflare Workers+Pages+D1:构建无服务器边缘应用完整方案

Cloudflare WorkersPagesD1:构建无服务器边缘应用完整方案 本教程通过制作一个简单的“留言板”,学习 Cloudflare 三个核心产品: Cloudflare Pages:部署前端页面Cloudflare Workers:编写后端 APICloudflare D1&#xf…

作者头像 李华
网站建设 2026/8/30 5:00:39

基于MATLAB的植保无人机全覆盖路径规划与遗传算法优化

简介:本资源是一套面向农业自动化与智能控制领域的MATLAB实践项目,专为具备基础编程与优化算法知识的高校学生、科研人员及农业无人机开发者设计,聚焦多无人机协同农药喷洒路径规划这一典型工程问题。压缩包共含10个文件(9个.m脚本…

作者头像 李华
网站建设 2026/8/30 5:00:05

PHP邮件发送管理系统源码:批量群发、SMTP配置与队列重试实战

简介:这是一套基于ThinkPHP框架开发的PHP邮件发送管理系统源码,面向Web开发者与运维人员,解决批量、可控、可监控的自动化邮件投递需求,适用于营销推广、通知提醒、用户注册验证等场景。资源包为ZIP格式,大小18.68MB&a…

作者头像 李华