第34课:Python|多进程、进程池、线程池原理与企业级实战用法

在这里插入图片描述

文章目录


📖 开篇导读

在上一节课中,我们学习了多线程编程,了解了Python中由于GIL(全局解释器锁)的存在,多线程在计算密集型任务中无法利用多核CPU,甚至可能因为锁竞争而比单线程还慢。但是对于IO密集型任务(网络请求、文件读写等),多线程仍然有显著提升。

那么,如果我们需要处理CPU密集型任务(大数计算、图像处理、机器学习训练等),想要充分利用多核CPU,应该怎么办?答案是:多进程(Multiprocessing)

多进程绕过GIL,每个进程拥有独立的Python解释器和内存空间,可以真正并行执行在多个CPU核心上。此外,Python还提供了**进程池(Pool)线程池(ThreadPoolExecutor)**来简化并发任务的管理,是生产环境的推荐方式。

💡 工作场景

  • 数据处理:对大规模数据集进行并行计算(如MapReduce思想)。
  • CI/CD:并行运行测试用例,减少构建时间。
  • 爬虫:多进程下载不同站点的页面,充分利用带宽。
  • Web后端:使用线程池处理大量IO请求(如Flask配合线程池)。

本课将学习:

  • multiprocessing模块创建进程
  • 进程间通信(QueuePipe、共享内存、Manager
  • 进程同步(锁、信号量等)
  • 进程池Poolapply_asyncmap方法
  • concurrent.futures模块中的ThreadPoolExecutorProcessPoolExecutor
  • 实战案例:并行文件处理、并行计算、多进程爬虫

学完本课,你将能够编写真正的并行程序,充分发挥多核CPU的性能。


🎯 学习目标

目标编号具体掌握内容对应面试/工作价值
1️⃣理解进程与线程的深层区别,掌握multiprocessing创建进程突破GIL限制,利用多核
2️⃣掌握进程间通信的多种方式(Queue、Pipe、共享内存、Manager)数据交换与同步
3️⃣熟练使用进程池Pool进行任务批量处理高效管理大量进程
4️⃣熟练使用concurrent.futures中的线程池和进程池统一接口简洁的并发编程
5️⃣了解进程同步机制(Lock、Semaphore)避免资源竞争
6️⃣能够根据任务类型选择多线程、多进程或异步IO架构设计能力

🔥 面试考点:“多进程和多线程的区别?”“什么是进程池?有什么用?”“multiprocessing.Poolmapapply_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:进程ID
  • name:进程名称
  • 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 共享内存(ValueArray

用于共享简单数据类型或数组,效率高,但需要手动同步。

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提供了更高级的共享对象,如listdict等,但性能较低。

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 批量异步:starmapstarmap_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。
  • 支持超时、回调等。

五、进程同步

类似于多线程,多进程也需要同步机制访问共享资源(尤其是ValueArray)。multiprocessing提供了LockRLockSemaphore等。

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}")

⚠️ 易错点避坑总结

序号坑点描述后果解决方案
1Windows下多进程代码未放在if __name__ == "__main__"无限递归创建进程,程序崩溃所有多进程启动代码都在该块内
2在进程间传递大对象(如大数据框架)序列化开销巨大,性能下降尽量使用共享内存或mmap
3使用Queue时忘记处理空队列异常导致进程阻塞或异常使用get(timeout)qsize()判断
4多进程访问共享文件未加锁文件内容错乱使用文件锁或数据库
5将多进程用于CPU密集型任务时等待结果不对正确使用,但注意join等待所有进程结束再收集结果
6ProcessPoolExecutor提交任务过多导致内存爆炸任务队列无限增长使用max_workers限制,或分批提交
7使用multiprocessing.Poolmap时,函数不能是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类,使用ManagerValuedict实现。启动10个进程,每个进程增加计数器10000次,验证最终结果是否为100000。


🧠 知识点思维导图总结

第34课:多进程与池

多进程基础

创建 Process

daemon 守护进程

join 等待

注意 Windows 使用 if name == 'main'

IPC

Queue 队列

Pipe 管道

Value/Array 共享内存

Manager 高级共享对象

进程池 Pool

批量 map / starmap

异步 apply_async

close / join

concurrent.futures

ThreadPoolExecutor

ProcessPoolExecutor

统一的 Future 接口

进程同步

Lock

RLock

Semaphore

选择指南

CPU密集 -> 多进程

IO密集 -> 多线程/异步

面试考点

多进程多线程区别

进程池优势

GIL对多进程无影响

Manager vs Queue


🔜 下节课预告

多进程和多线程都是并发编程的重要手段。随着异步编程的流行,Python还提供了asyncio库,使用协程实现高并发的IO任务。下一节课我们将学习协程与异步编程

第35课:协程、async/await异步编程零基础从入门到实战

内容包括:

  • 协程的概念与生成器的演进
  • async/await语法
  • 事件循环与asyncio
  • 异步IO与同步IO对比
  • 实战:异步网络请求、Web爬虫

异步编程是高性能IO应用的核心,掌握它你将能写出高效的网络服务。

🌟 学习鼓励:多进程是Python突破GIL限制、充分利用多核CPU的利器。虽然多进程内存开销大,但通过进程池和共享内存可以高效管理。请务必亲手运行进程池和concurrent.futures的案例,体会两种API的区别和适用场景。多进程与多线程结合使用,能让你的程序性能成倍提升!


🔗《50节课 Python 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值