
文章目录
📖 开篇导读
在上一节课中,我们学习了多线程编程,了解了Python中由于GIL(全局解释器锁)的存在,多线程在计算密集型任务中无法利用多核CPU,甚至可能因为锁竞争而比单线程还慢。但是对于IO密集型任务(网络请求、文件读写等),多线程仍然有显著提升。
那么,如果我们需要处理CPU密集型任务(大数计算、图像处理、机器学习训练等),想要充分利用多核CPU,应该怎么办?答案是:多进程(Multiprocessing)。
多进程绕过GIL,每个进程拥有独立的Python解释器和内存空间,可以真正并行执行在多个CPU核心上。此外,Python还提供了**进程池(Pool)和线程池(ThreadPoolExecutor)**来简化并发任务的管理,是生产环境的推荐方式。
💡 工作场景:
- 数据处理:对大规模数据集进行并行计算(如MapReduce思想)。
- CI/CD:并行运行测试用例,减少构建时间。
- 爬虫:多进程下载不同站点的页面,充分利用带宽。
- Web后端:使用线程池处理大量IO请求(如Flask配合线程池)。
本课将学习:
multiprocessing模块创建进程- 进程间通信(
Queue、Pipe、共享内存、Manager) - 进程同步(锁、信号量等)
- 进程池
Pool和apply_async、map方法 concurrent.futures模块中的ThreadPoolExecutor和ProcessPoolExecutor- 实战案例:并行文件处理、并行计算、多进程爬虫
学完本课,你将能够编写真正的并行程序,充分发挥多核CPU的性能。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 理解进程与线程的深层区别,掌握multiprocessing创建进程 | 突破GIL限制,利用多核 |
| 2️⃣ | 掌握进程间通信的多种方式(Queue、Pipe、共享内存、Manager) | 数据交换与同步 |
| 3️⃣ | 熟练使用进程池Pool进行任务批量处理 | 高效管理大量进程 |
| 4️⃣ | 熟练使用concurrent.futures中的线程池和进程池统一接口 | 简洁的并发编程 |
| 5️⃣ | 了解进程同步机制(Lock、Semaphore) | 避免资源竞争 |
| 6️⃣ | 能够根据任务类型选择多线程、多进程或异步IO | 架构设计能力 |
🔥 面试考点:“多进程和多线程的区别?”“什么是进程池?有什么用?”“
multiprocessing.Pool的map和apply_async区别?”“共享内存的使用场景?”“如何实现进程安全的计数器?”
📚 知识点理论精讲
一、多进程基础
1.1 为什么需要多进程?
- 绕过GIL:每个进程有独立的GIL,可以真正的并行执行。
- 隔离性:进程崩溃不影响其他进程,稳定性更高。
- 劣势:创建和销毁进程开销大,进程间通信相对复杂。
1.2 创建进程
使用multiprocessing.Process,与threading.Thread用法类似。
from multiprocessing import Process
import os
def worker(name):
print(f"子进程 {name},PID: {os.getpid()}")
if __name__ == "__main__":
p = Process(target=worker, args=("A",))
p.start()
p.join()
注意:在Windows上必须将多进程代码放在if __name__ == "__main__":块内,否则会无限递归创建进程。
1.3 进程中的常用属性
pid:进程IDname:进程名称daemon:是否守护进程(主进程结束时,子进程自动终止)join():等待进程结束
二、进程间通信(IPC)
由于进程内存独立,不能直接共享变量。Python提供了多种IPC机制。
2.1 队列(Queue)
multiprocessing.Queue类似于线程队列,但用于进程间安全传递数据。
from multiprocessing import Process, Queue
def producer(q):
q.put("Hello")
def consumer(q):
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()
2.2 管道(Pipe)
Pipe()返回两个连接对象,duplex=True表示双向通信。
from multiprocessing import Process, Pipe
def sender(conn):
conn.send("消息")
def receiver(conn):
msg = conn.recv()
print(msg)
if __name__ == "__main__":
parent_conn, child_conn = Pipe()
p1 = Process(target=sender, args=(parent_conn,))
p2 = Process(target=receiver, args=(child_conn,))
p1.start(); p2.start()
p1.join(); p2.join()
2.3 共享内存(Value、Array)
用于共享简单数据类型或数组,效率高,但需要手动同步。
from multiprocessing import Process, Value, Array
def increment(val, arr):
val.value += 1
for i in range(len(arr)):
arr[i] += 1
if __name__ == "__main__":
v = Value('i', 0) # 'i'表示整数
a = Array('i', [0,0,0])
p = Process(target=increment, args=(v, a))
p.start()
p.join()
print(v.value, a[:])
2.4 管理器(Manager)
Manager提供了更高级的共享对象,如list、dict等,但性能较低。
from multiprocessing import Process, Manager
def add_data(d, key, value):
d[key] = value
if __name__ == "__main__":
with Manager() as manager:
d = manager.dict()
p = Process(target=add_data, args=(d, "name", "张三"))
p.start()
p.join()
print(d)
三、进程池(Pool)
频繁创建销毁进程开销大,进程池预先创建一批进程,重复使用。
3.1 基础使用
from multiprocessing import Pool
import time
def square(x):
return x * x
if __name__ == "__main__":
with Pool(processes=4) as pool:
results = pool.map(square, range(10))
print(results) # [0,1,4,9,16,25,36,49,64,81]
3.2 异步方法apply_async
用于非阻塞提交任务,返回AsyncResult对象。
def f(x):
return x * x
with Pool(4) as pool:
result = pool.apply_async(f, (10,))
print(result.get(timeout=1))
3.3 批量异步:starmap和starmap_async
starmap用于带多个参数的情况。
def add(x, y):
return x + y
with Pool(4) as pool:
results = pool.starmap(add, [(1,2), (3,4), (5,6)])
print(results) # [3,7,11]
四、concurrent.futures:高级并发接口
Python 3.2+ 提供了concurrent.futures模块,统一了线程池和进程池的API。
4.1 线程池ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor
import time
def task(n):
time.sleep(1)
return n * n
with ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(task, i) for i in range(10)]
results = [f.result() for f in futures]
4.2 进程池ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
def square(x):
return x * x
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(square, range(10)))
4.3 优势
- 统一的API,容易在进程池和线程池间切换。
- 使用
as_completed等待最早完成的Future。 - 支持超时、回调等。
五、进程同步
类似于多线程,多进程也需要同步机制访问共享资源(尤其是Value和Array)。multiprocessing提供了Lock、RLock、Semaphore等。
from multiprocessing import Process, Lock, Value
def increment(v, lock):
for _ in range(1000):
with lock:
v.value += 1
if __name__ == "__main__":
v = Value('i', 0)
lock = Lock()
procs = [Process(target=increment, args=(v, lock)) for _ in range(10)]
for p in procs: p.start()
for p in procs: p.join()
print(v.value) # 10000
六、多进程与多线程的选择指南
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| CPU密集型(计算、图像处理、机器学习) | 多进程(ProcessPoolExecutor) | 绕过GIL,利用多核 |
| IO密集型(网络请求、文件读写、数据库) | 多线程(ThreadPoolExecutor)或异步IO | 线程切换开销小,易编程 |
| 高并发、短任务 | 线程池 | 创建线程开销小 |
| 长期运行、计算量大 | 进程池 或 手动管理进程 | 稳定性好 |
💻 代码案例实操
案例1:基础多进程与进程间通信(Queue)
"""
basic_multiprocessing.py
演示创建多个进程,使用Queue传递数据
"""
from multiprocessing import Process, Queue
import os, time
def worker(q, name):
"""工作进程:从队列获取数据并处理"""
while True:
item = q.get()
if item is None:
break
print(f"进程 {name}(PID:{os.getpid()}) 处理 {item}")
time.sleep(0.5)
print(f"进程 {name} 退出")
def producer(q):
"""主进程产生任务"""
for i in range(10):
q.put(f"任务-{i}")
time.sleep(0.2)
# 发送结束信号
for _ in range(3): # 3个Worker
q.put(None)
if __name__ == "__main__":
q = Queue()
workers = []
for i in range(3):
p = Process(target=worker, args=(q, f"Worker{i+1}"))
workers.append(p)
p.start()
producer(q)
for p in workers:
p.join()
print("所有进程结束")
案例2:使用Pipe双向通信
"""
pipe_communication.py
父进程与子进程通过Pipe通信
"""
from multiprocessing import Process, Pipe
def child(conn):
"""子进程:接收命令,返回结果"""
while True:
cmd = conn.recv()
if cmd == "exit":
conn.send("bye")
break
elif cmd == "hello":
conn.send("world")
else:
conn.send(f"unknown command: {cmd}")
if __name__ == "__main__":
parent_conn, child_conn = Pipe()
p = Process(target=child, args=(child_conn,))
p.start()
for cmd in ["hello", "ping", "exit"]:
parent_conn.send(cmd)
response = parent_conn.recv()
print(f"cmd: {cmd} -> response: {response}")
p.join()
案例3:共享内存Value和Array加速计算
"""
shared_memory.py
使用共享内存实现多进程并行累加
"""
from multiprocessing import Process, Value, Array, Lock
def add_partial(v, arr, start, end, lock):
"""计算部分和,并累加到共享变量"""
partial = sum(arr[start:end])
with lock:
v.value += partial
if __name__ == "__main__":
import random
n = 1000000
arr = Array('i', [random.randint(1,100) for _ in range(n)])
v = Value('i', 0)
lock = Lock()
num_processes = 4
chunk_size = n // num_processes
processes = []
for i in range(num_processes):
start = i * chunk_size
end = start + chunk_size
if i == num_processes - 1:
end = n
p = Process(target=add_partial, args=(v, arr, start, end, lock))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"并行计算总和: {v.value}")
print(f"实际总和: {sum(arr)}")
案例4:进程池Pool实现并行文件处理
"""
pool_file_process.py
使用进程池并行统计多个文件的行数
"""
from multiprocessing import Pool
import os
def count_lines(filename):
"""统计文件行数"""
if not os.path.exists(filename):
return (filename, -1)
with open(filename, 'r', encoding='utf-8') as f:
count = sum(1 for _ in f)
return (filename, count)
if __name__ == "__main__":
# 创建测试文件
for i in range(10):
with open(f"test_{i}.txt", "w") as f:
f.write("\n".join(f"line{j}" for j in range(i*100)))
files = [f"test_{i}.txt" for i in range(10)]
with Pool(processes=4) as pool:
results = pool.map(count_lines, files)
for filename, lines in results:
print(f"{filename}: {lines}行")
# 清理
for i in range(10):
os.remove(f"test_{i}.txt")
案例5:使用concurrent.futures的线程池和进程池对比
"""
futures_compares.py
对比ThreadPoolExecutor和ProcessPoolExecutor处理CPU密集型任务
"""
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def fib(n):
"""计算Fibonacci数(递归版本,CPU密集)"""
if n < 2:
return n
return fib(n-1) + fib(n-2)
def run_threadpool(n, workers=4):
with ThreadPoolExecutor(max_workers=workers) as executor:
list(executor.map(fib, [n]*workers))
def run_processpool(n, workers=4):
with ProcessPoolExecutor(max_workers=workers) as executor:
list(executor.map(fib, [n]*workers))
if __name__ == "__main__":
n = 35 # 较大数,使计算明显
workers = 4
start = time.time()
run_threadpool(n, workers)
print(f"ThreadPoolExecutor耗时: {time.time() - start:.2f}s")
start = time.time()
run_processpool(n, workers)
print(f"ProcessPoolExecutor耗时: {time.time() - start:.2f}s")
案例6:进程池异步获取结果(apply_async)
"""
pool_async.py
使用apply_async进行非阻塞任务提交
"""
from multiprocessing import Pool
import time
def slow_square(x):
time.sleep(0.5)
return x * x
if __name__ == "__main__":
with Pool(4) as pool:
# 提交异步任务
async_results = [pool.apply_async(slow_square, (i,)) for i in range(10)]
# 可以继续做其他事情
print("任务已提交,等待结果...")
# 获取结果
for i, ar in enumerate(async_results):
# get()会阻塞直到结果就绪,可以设置超时
result = ar.get(timeout=10)
print(f"result[{i}] = {result}")
案例7:Manager共享字典实现进程安全计数器
"""
manager_dict_counter.py
使用Manager.dict协调多个进程修改字典
"""
from multiprocessing import Process, Manager
import time
def worker(d, lock, key, increment):
"""增加字典中key的值"""
for _ in range(100):
with lock:
d[key] = d.get(key, 0) + increment
time.sleep(0.001) # 模拟工作
if __name__ == "__main__":
with Manager() as manager:
d = manager.dict()
lock = manager.Lock() # 也可以使用multiprocessing.Lock
processes = []
for i in range(5):
p = Process(target=worker, args=(d, lock, "count", 1))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"最终计数: {d.get('count', 0)}") # 应为500
案例8:多进程爬取网页(模拟)
"""
parallel_webcrawler.py
使用进程池模拟并发请求多个URL(实际网络请求用多线程更好,此处仅演示)
"""
from multiprocessing import Pool
import time
import random
def fetch_url(url):
"""模拟请求网页,返回状态码"""
print(f"开始请求 {url}")
time.sleep(random.uniform(0.2, 0.8)) # 模拟网络延迟
status = 200 if random.random() > 0.1 else 404
print(f"完成 {url} -> {status}")
return (url, status)
if __name__ == "__main__":
urls = [
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
"https://example.com/page4",
"https://example.com/page5",
]
with Pool(3) as pool:
results = pool.map(fetch_url, urls)
print("\n结果汇总:")
for url, status in results:
print(f"{url}: {status}")
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | Windows下多进程代码未放在if __name__ == "__main__"内 | 无限递归创建进程,程序崩溃 | 所有多进程启动代码都在该块内 |
| 2 | 在进程间传递大对象(如大数据框架) | 序列化开销巨大,性能下降 | 尽量使用共享内存或mmap |
| 3 | 使用Queue时忘记处理空队列异常 | 导致进程阻塞或异常 | 使用get(timeout)或qsize()判断 |
| 4 | 多进程访问共享文件未加锁 | 文件内容错乱 | 使用文件锁或数据库 |
| 5 | 将多进程用于CPU密集型任务时等待结果不对 | 正确使用,但注意join | 等待所有进程结束再收集结果 |
| 6 | ProcessPoolExecutor提交任务过多导致内存爆炸 | 任务队列无限增长 | 使用max_workers限制,或分批提交 |
| 7 | 使用multiprocessing.Pool的map时,函数不能是lambda | 序列化失败 | 定义普通函数 |
| 8 | 在子进程中重新导入主模块导致无限循环 | Windows问题 | 同样需要保护主模块 |
| 9 | 共享Manager对象性能差 | 大量数据交换时慢 | 使用Queue或共享内存 |
| 10 | 进程中使用了非picklable对象(如文件句柄)作为参数 | pickle.PickleError | 传递路径字符串,在进程中自己打开 |
📝 课后实战练习题
第1题:多进程计算素数
编写程序,使用多进程计算1-1000000之间有多少个素数。拆分任务到4个进程,每个进程处理一部分,汇总结果。对比单进程耗时。
第2题:进程池批量处理图像(模拟)
假设有100张图片需要处理(每个图片处理耗时0.1秒),使用进程池(4个进程)处理,输出总耗时。模拟处理函数只需sleep。
第3题:生产者-消费者多进程版
使用multiprocessing.Queue实现一个多进程的生产者-消费者模型:一个生产者生产随机数,3个消费者处理(打印)。生产者生产20个数字后发送结束信号。
第4题:使用concurrent.futures重构
将第2题的进程池改成ProcessPoolExecutor实现,并比较代码简洁度。
第5题:多进程下载图片(真实或模拟)
模拟从URL列表下载图片,每个下载耗时随机。使用进程池下载,并统计成功率。注意:进程池不适合网络IO密集型,但可作为练习。
第6题:矩阵乘法并行
实现两个矩阵的乘法,将计算划分到多个进程中(按行划分)。较大矩阵如1000x1000,对比单进程和多进程时间。
第7题:使用Manager实现进程安全的计数器
定义一个Counter类,使用Manager的Value或dict实现。启动10个进程,每个进程增加计数器10000次,验证最终结果是否为100000。
🧠 知识点思维导图总结
🔜 下节课预告
多进程和多线程都是并发编程的重要手段。随着异步编程的流行,Python还提供了asyncio库,使用协程实现高并发的IO任务。下一节课我们将学习协程与异步编程。
第35课:协程、async/await异步编程零基础从入门到实战
内容包括:
- 协程的概念与生成器的演进
async/await语法- 事件循环与
asyncio库 - 异步IO与同步IO对比
- 实战:异步网络请求、Web爬虫
异步编程是高性能IO应用的核心,掌握它你将能写出高效的网络服务。
🌟 学习鼓励:多进程是Python突破GIL限制、充分利用多核CPU的利器。虽然多进程内存开销大,但通过进程池和共享内存可以高效管理。请务必亲手运行进程池和
concurrent.futures的案例,体会两种API的区别和适用场景。多进程与多线程结合使用,能让你的程序性能成倍提升!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

356

被折叠的 条评论
为什么被折叠?



