当前位置: 首页 > news >正文

Day 25:【99天精通Python】多进程编程 - 榨干CPU的每一滴性能

Day 25:【99天精通Python】多进程编程 - 榨干CPU的每一滴性能

前言

欢迎来到第25天!

在昨天(多线程)的课程中,我们发现了一个令人沮丧的事实:由于 GIL(全局解释器锁)的存在,Python 的多线程在处理CPU密集型任务(如复杂的数学计算、视频转码、图像处理)时,并没有起到加速作用,甚至可能因为上下文切换而变得更慢。

今天,我们要打破这个枷锁。多进程 (Multiprocessing)允许我们启动多个独立的 Python 解释器进程。每个进程都有自己独立的 GIL 和内存空间,它们可以运行在不同的 CPU 核心上,真正实现并行计算

如果说多线程是"一个工厂里招了多个工人"(还是只有一个车间主任指挥),那么多进程就是"开了好几个分厂"。

本节内容:

  • 进程与线程的区别回顾
  • multiprocessing模块基础
  • 为什么必须写if __name__ == '__main__':
  • 进程间通信 (Queue)
  • 进程池 (Pool) 的高效使用
  • 实战练习:多核并行计算素数

一、多进程基础:Process 类

Python 的multiprocessing模块提供了与threading类似的 API,上手非常容易。

1.1 启动一个进程

importmultiprocessingimporttimeimportosdeftask(name):# os.getpid() 获取当前进程ID# os.getppid() 获取父进程IDprint(f"进程{name}启动 (PID:{os.getpid()}, 父PID:{os.getppid()})")time.sleep(2)print(f"进程{name}结束")if__name__=='__main__':print(f"主进程启动 (PID:{os.getpid()})")# 创建进程p=multiprocessing.Process(target=task,args=("Worker-1",))# 启动p.start()# 等待结束p.join()print("主进程结束")

1.2 重要:Windows 下的注意事项

在 Windows 上使用多进程,必须将启动代码放在if __name__ == '__main__':之下。
原因:Windows 创建进程时会导入父模块,如果不加保护,会导致无限递归创建进程,程序直接崩坏。


二、数据隔离:进程之间不共享全局变量

这是多进程与多线程最大的区别。每个进程都有自己独立的内存空间。

importmultiprocessing# 全局变量money=100defchange_money():globalmoney money=0print(f"子进程修改后的 money:{money}")if__name__=='__main__':p=multiprocessing.Process(target=change_money)p.start()p.join()print(f"主进程的 money:{money}")# 结果依然是 100!子进程改的是它自己那份拷贝。

三、进程间通信 (IPC)

既然内存不共享,那进程间怎么传递数据呢?最常用的方式是队列 (Queue)
注意:这里的 Queue 是multiprocessing.Queue,不是线程的queue.Queue

frommultiprocessingimportProcess,Queuedefproducer(q):print("生产者:放入数据...")q.put("Hello")q.put("World")defconsumer(q):print("消费者:准备取数据...")print(q.get())print(q.get())if__name__=='__main__':# 创建一个共享队列q=Queue()p1=Process(target=producer,args=(q,))p2=Process(target=consumer,args=(q,))p1.start()p2.start()p1.join()p2.join()

四、进程池 (Pool):批量管理进程

如果你有 100 个任务要处理,不可能启动 100 个进程(系统资源会耗尽)。通常我们会创建一个进程池,比如池子里有 4 个进程,它们轮流处理这 100 个任务。

4.1 使用 Pool.map (同步/简单)

frommultiprocessingimportPoolimporttimeimportosdefheavy_work(n):print(f"进程{os.getpid()}正在计算{n}^2")time.sleep(1)# 模拟耗时returnn*nif__name__=='__main__':# 创建包含 4 个进程的池# 默认不填则是 CPU 核心数withPool(4)asp:# map 会自动把任务分发给进程池,并收集结果results=p.map(heavy_work,range(10))print(f"计算结果:{results}")

4.2 使用 Pool.apply_async (异步/灵活)

如果不希望阻塞主进程,或者任务参数复杂,可以使用apply_async

if__name__=='__main__':p=Pool(4)results=[]foriinrange(5):# 异步提交任务,返回一个 result 对象res=p.apply_async(heavy_work,args=(i,))results.append(res)p.close()# 禁止再提交新任务p.join()# 等待所有任务完成# 获取结果final_values=[res.get()forresinresults]print(final_values)

五、实战练习:多进程计算素数

让我们看看多进程在 CPU 密集型任务上到底有多强。
任务:计算 1 到 100,000 之间所有素数的个数。

importtimefrommultiprocessingimportPool,cpu_countdefis_prime(n):ifn<=1:returnFalseforiinrange(2,int(n**0.5)+1):ifn%i==0:returnFalsereturnTruedefcount_primes(start,end):count=0foriinrange(start,end):ifis_prime(i):count+=1returncountif__name__=='__main__':# 任务范围TOTAL_NUMS=200000# --- 单进程版本 ---start_t=time.time()result=count_primes(1,TOTAL_NUMS)print(f"单进程耗时:{time.time()-start_t:.2f}s, 结果:{result}")# --- 多进程版本 ---start_t=time.time()# 将任务拆分成 4 份 (假设 CPU 是 4 核)pool_size=4step=TOTAL_NUMS//pool_size ranges=[]foriinrange(pool_size):start=1+i*step end=1+(i+1)*step ranges.append((start,end))# ranges = [(1, 50000), (50001, 100000), ...]withPool(pool_size)asp:# starmap 支持传多个参数results=p.starmap(count_primes,ranges)total=sum(results)print(f"多进程耗时:{time.time()-start_t:.2f}s, 结果:{total}")

预期结果
在多核 CPU 上,多进程版本的速度通常是单进程的 2-4 倍(取决于核心数)。


六、常见问题

Q1:进程是不是越多越好?

不是。

  1. 开销大:创建和销毁进程比线程消耗大得多。
  2. 上下文切换:如果进程数超过 CPU 核心数太多,CPU 就要频繁在进程间切换,反而降低效率。
    最佳实践:进程池大小通常设置为cpu_count()cpu_count() + 1

Q2:进程间通信还有其他方式吗?

除了Queue,还有Pipe(管道,适合两个进程通信)、Manager(可以创建共享的列表、字典,但速度较慢)。


七、小结

多进程 Multiprocessing

Process 类

数据隔离

进程池 Pool

Process(target=...)

start() / join()

全局变量不共享

通信: Queue

Pool(processes=4)

map / starmap (批量)

apply_async (异步)

关键要点

  1. CPU 密集型任务(计算)首选多进程
  2. I/O 密集型任务(网络)首选多线程
  3. 多进程必须注意if __name__ == '__main__':保护。
  4. 进程间数据默认隔离,需通过Queue传递。

八、课后作业

  1. 文件搜索:编写一个多进程程序,在指定的文件夹(包含大量子文件夹)中搜索包含特定关键词的所有.txt文件。主进程负责汇总所有找到的文件路径。
  2. 图片缩略图生成:假设有一个文件夹里有 100 张高清大图。编写程序使用Process Pool并行地将它们缩小为 100x100 的缩略图。(提示:可以使用Pillow库处理图片,需先pip install pillow)。
  3. 多进程进度条:尝试实现一个功能,主进程显示一个总进度条,反映后台多个进程任务的整体完成情况(使用QueueManager传递进度)。

下节预告

Day 26:网络编程入门 (Socket)- 玩转了单机并发,接下来我们要跨越网络,让两台电脑互相聊天!


系列导航

  • 上一篇:Day 24 - 多线程编程
  • 下一篇:Day 26 - 网络编程入门(待更新)
http://www.jsqmd.com/news/235799/

相关文章:

  • 每日面试题分享132:什么是Vue中的slot?它的作用是什么?
  • 每日面试题分享133:在Vue模版渲染时,如何保留HTML注释?
  • ES数据库节点故障处理:实战案例详解
  • Java SpringBoot+Vue3+MyBatis 中小型医院网站系统源码|前后端分离+MySQL数据库
  • 谷歌商家中心 (Google Merchant Center) VS 产品数据 Feed 新手指南 VS 结构化数据Schmea
  • SpringBoot+Vue 桂林旅游景点导游平台平台完整项目源码+SQL脚本+接口文档【Java Web毕设】
  • Day 26:【99天精通Python】网络编程入门 (Socket) - 让电脑互相“打电话“
  • ⚡_实时系统性能优化:从毫秒到微秒的突破[20260112171643]
  • 【毕业设计】SpringBoot+Vue+MySQL 网站平台源码+数据库+论文+部署文档
  • Packet Tracer运行环境配置全面讲解
  • Day 27:【99天精通Python】HTTP协议与Requests库 - 爬虫与API的敲门砖
  • 什么是天猫国际品牌代理运营?一般代运营提供哪些服务?
  • 每日一个C++知识点|const 和 constexpr 的区别
  • screen命令在断网环境下的调试应用操作指南
  • [特殊字符]_Web框架性能终极对决:谁才是真正的速度王者[20260112172541]
  • 超详细版LVGL教程:从零实现家居主界面
  • 工业控制面板中LCD1602的布局与驱动技巧
  • 深耕香港会计服务领域 香港卓信会计打造企业注册一站式解决方案
  • 天猫TP公司是什么意思?一般提供哪些服务?
  • RealMem: 重新定义AI的“长期记忆”,挑战真实场景交互
  • 手把手教程:Elasticsearch下载与Logstash环境搭建
  • 【AI机器视觉】MediaPile和YOLO对比
  • MDK与工业自动化集成:系统学习手册
  • 解析USB3.0接口定义引脚说明中的盲埋孔使用技巧
  • Java Web 智能物流管理系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】
  • Elasticsearch客户端集成:应用层对接实战案例
  • USB3.0接口引脚定义详解:从基础到应用完整指南
  • [特殊字符]_高并发场景下的框架选择:从性能数据看技术决策[20260112170745]
  • 微服务分布式SpringBoot+Vue+Springcloud人口老龄化社区活动老年人服务和管理平台
  • Java Web 电影评论网站系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】